feat(server-detail): display + edit headers/env, with reveal & convert-to-secret#466
Merged
Conversation
Deploying mcpproxy-docs with
|
| Latest commit: |
7151911
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://ba67cde1.mcpproxy-docs.pages.dev |
| Branch Preview URL: | https://feat-server-detail-headers-e.mcpproxy-docs.pages.dev |
📦 Build ArtifactsWorkflow Run: View Run Available Artifacts
How to DownloadOption 1: GitHub Web UI (easiest)
Option 2: GitHub CLI gh run download 25857898404 --repo smart-mcp-proxy/mcpproxy-go
|
18 tasks
Headers and env were dropped at the runtime -> management -> contract
boundary, so neither the Web UI nor the macOS tray could render or
round-trip them. A server configured with a static Authorization header
(e.g. synapbus with a Bearer token) appeared with no headers section at
all on the Web UI Config tab.
Backend wiring (this commit, all surfaces):
- internal/runtime/runtime.go: GetAllServers emits headers and env from
serverStatus.Config in serverMap.
- internal/management/service.go: ListServers extracts headers and env
in both typed (map[string]string) and generic (map[string]interface{})
shapes. Existing redaction at the HTTP layer continues to apply.
Web UI (Config tab):
- New Headers card with redact-by-default + click-to-reveal, per-row
inline edit/delete, an Add row, and a "Convert to secret" button that
stores the literal value in the OS keyring and rewrites the field as
`${keyring:<name>}`.
- Same affordances applied to the Environment Variables card.
- New reusable KVValueCell component encapsulates the per-row UX so
Headers and Env share the same display/edit/reveal/convert logic.
- api.ts: new patchServer() and storeSecret() helpers wrapping
PATCH /api/v1/servers/{id} and POST /api/v1/secrets respectively.
- types/api.ts: add `headers` to Server interface (the contracts.ts
twin already had it; api.ts had drifted).
Note on reveal: backend redaction replaces sensitive header values with
`***REDACTED***` unless `reveal_secret_headers: true` is set in config.
The KVValueCell detects that sentinel and disables both reveal and the
"Convert to secret" button (since neither has the real value in hand),
surfacing a tooltip that points the user at the config flag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rt modal
Vue templates do NOT interpret \${...} as JS template-literal syntax —
the dollar-and-braces inside the modal body were rendering verbatim as
'${'{'}'... text. Replace the awkward \${'{'} escape with a plain string
interpolation through {{ '...' }} mustache so the user sees the
actual reference syntax they're about to substitute into config.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds full round-trip support for headers and env on the macOS tray's
server detail screen, matching the new Web UI experience.
API model:
- ServerStatus gains `headers` and `env` (both [String: String]?).
CodingKeys updated to map the JSON `headers` and `env` fields the Go
backend now emits (companion to the runtime + management wiring in
the parent commit).
View:
- Headers section under Connection for HTTP / streamable-http servers,
visible in both view mode (sorted key list with masked values) and
edit mode (KEY=VALUE textarea, parallel to the existing env textarea).
- editEnvVars now pre-populates from `server.env` instead of starting
empty — fixes the long-standing stub at L939 that explicitly noted
the missing config API.
- editHeaders works the same way for headers.
- saveEdits() sends both maps unconditionally so deletes round-trip;
refuses to save if any header value is still `***REDACTED***` (the
backend sentinel emitted when `reveal_secret_headers: false`) so we
don't silently overwrite a real secret with the placeholder.
- New helpers: parseKVTextarea (shared between env and headers) and
maskedHeaderValue (recognises `${keyring:...}` and `${env:...}`
references and renders them as-is, masks literal values, surfaces the
redaction sentinel verbatim so users know to flip
reveal_secret_headers in their config).
Convert-to-secret in Swift: deferred. The Web UI surfaces this per row
through KVValueCell; the equivalent SwiftUI experience would need a new
modal + state machine that doesn't fit the existing textarea-based
edit form. Tracked as a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rdening
User feedback on the previous macOS commit triggered three follow-up fixes
that all land naturally together:
Sidebar cap (MainWindow.swift):
- NavigationSplitView only set `min: 180, ideal: 220` for the sidebar
column. SwiftUI was free to expand it unbounded after some layout
transitions, squeezing the detail pane to a sliver on the right.
- Add `max: 280` so the sidebar stays at a sensible width while the
detail pane gets the rest. Verified visually on the server detail
Config tab.
Redacted-save guard (ServerDetailView.swift::saveEdits):
- Previous version only refused to save when the textarea still
contained `***REDACTED***` literally. A user could still delete the
redacted line, add a new header, and silently wipe out the real
Authorization behind the redaction.
- Now we diff the new headers map against the original `server.headers`
and refuse the save when either (a) a `***REDACTED***` literal is
still present OR (b) any key whose original value was redacted is
missing entirely from the new map. The error message lists the
offending keys and points the user at `reveal_secret_headers: true`.
Convert-to-secret on macOS (the previously-deferred work):
- New SwiftUI sheet binding through `@State convertSheet:
ConvertToSecretContext?`. The sheet body mirrors the Web UI flow —
pre-suggests a secret name (`<server-name>-<key>`, lowercased and
hyphen-sanitised), shows a live `${keyring:NAME}` preview, has
Cancel + Convert with proper keyboard shortcuts.
- Headers view-mode rendering switched from the static
`configRow(label:value:)` to a new `kvRow(scope:key:value:)` that
renders `${keyring:…}` / `${env:…}` references as a capsule chip
with no actions, surfaces the `***REDACTED***` sentinel verbatim
(still no convert button — we don't have the real value), and shows
a 🔒 button for genuine literal values that opens the convert sheet.
- Environment Variables now also renders in view mode (previously it
was edit-only) with the same kvRow affordances. Visible whenever the
server has any env vars, regardless of protocol — stdio servers can
finally inspect their pre-populated env without entering edit mode.
- APIClient gains `storeSecret(name:value:)` wrapping POST
/api/v1/secrets which returns the `${keyring:<name>}` reference
string to substitute back into the server config via the existing
`updateServer` PATCH.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…et_headers to MCP tool only User feedback on the previous round: the Server Detail edit form refused to save when any header value still contained the `***REDACTED***` sentinel, and pushed users at the `reveal_secret_headers: true` config flag to unblock themselves. That trade-off was wrong — it punished the human UI to protect an agent code path. The actual threat model: - REST API (`/api/v1/servers`, SSE `/events`): gated by the local per-install API key. Same trust boundary as access to `~/.mcpproxy/mcp_config.json` on disk where these values are already stored in plaintext. Redacting in the response bought no real security, only broke the Web UI + macOS tray edit-and-save round-trip. - MCP `upstream_servers` tool: invoked by AI agents, output gets read back to the LLM context. THIS is the agent-context exposure `reveal_secret_headers` was created to protect — and that redaction was already implemented separately in `internal/server/mcp.go:~2545`, unaffected by this change. Backend changes: - internal/httpapi/server.go: drop `redactServerHeaders` calls in `handleGetServers` (both code paths) and remove the now-unused method. - internal/runtime/event_bus.go: drop the `redactServerHeaders` call on SSE `servers.changed` payloads and remove the now-unused method. - internal/runtime/event_bus_payload_test.go: rewrite the redaction test to assert the new policy — SSE must now carry plaintext, the test name changes from `…_RedactsSensitiveHeaders` to `…_SendsPlaintextHeaders`. - internal/config/config.go: update the `RevealSecretHeaders` doc to explicitly scope the flag to the MCP tool. REST + SSE always send plaintext from now on. UI cleanup (the redaction sentinel is no longer expected on the wire): - macOS Swift `saveEdits()`: drop the elaborate redacted-save guard (refused save when ***REDACTED*** literal still in textarea OR a redacted key was deleted). Both cases became impossible once the REST API serves plaintext. - macOS Swift `kvRow()`: drop the `value != "***REDACTED***"` check that disabled the Convert-to-secret button. - macOS Swift `performConvertToSecret()`: drop the matching guard. - Vue `KVValueCell.vue`: drop the `isBackendRedacted` computed flag and the "Backend redacted this value, set reveal_secret_headers" tooltip. Reveal and Convert are always available on literal values. - Vue `ServerDetail.vue::commitConvert()`: drop the same guard. Verified end-to-end on macOS and Web UI: - REST: `curl /api/v1/servers` → `Authorization: Bearer 1d386f...` (the real 71-char token). - MCP tool: `upstream_servers list` → `"Authorization":"***REDACTED***"` (still hidden from agents). - macOS Server Detail → synapbus → Edit → Headers textarea pre-populated with the real Bearer token; Save no longer blocked. - Web UI synapbus Config → Headers row shows `••••59 (71 chars)` with reveal eye, Convert-to-secret 🔒, edit, delete — all functional. OAS spec regenerated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback on the previous commit: dropping REST/SSE redaction unblocked the UI but narrowed the threat model from PR #425 (a local process that holds the API key but not filesystem access could read other upstreams' Bearer tokens off the wire). The better trade-off is to keep both halves of PR #425 intact AND let the UI edit without round-tripping the redacted sentinel. Solution: deep-merge PATCH semantics so the client sends only the keys that changed. Redacted-but-untouched values stay out of the patch entirely, the backend keeps the real string on disk. Backend: - internal/httpapi/server.go::handlePatchServer — when the request body contains `headers` or `env`, MERGE into the existing stored map instead of replacing. New `headers_remove` / `env_remove` fields carry explicit deletes. Sending both is allowed (deletes apply after upserts). - internal/httpapi/server.go::AddServerRequest — adds the two `*_remove` fields with a comment documenting the new semantics. The add path ignores them. - internal/httpapi/server.go::handleGetServers — re-enable `redactServerHeaders` on both code paths. - internal/runtime/event_bus.go::emitServersChanged — re-enable redaction on SSE `servers.changed` payloads. SSE rides the same trust boundary as the REST GET. - internal/config/config.go::RevealSecretHeaders — restore the original PR #425 doc (REST + MCP both gated) with a new paragraph pointing at the deep-merge mechanism that makes the UI work anyway. Tests: - internal/httpapi/patch_server_test.go — 4 new tests pinning the merge semantics: - TestHandlePatchServer_HeadersDeepMerge — `headers: {X-New: v}` against existing `{Authorization, X-Trace}` preserves both original keys and adds X-New. - TestHandlePatchServer_HeadersRemove — `headers_remove: [...]` deletes the listed keys. - TestHandlePatchServer_HeadersSetAndRemove — both fields in one PATCH; deletes apply after upserts. - TestHandlePatchServer_EnvDeepMerge — same pattern for env. - internal/runtime/event_bus_payload_test.go — restored the original PR #425 assertion (`...RedactsSensitiveHeaders`). Frontend (Web UI): - ServerDetail.vue — `patchServerDiff(patch, action)` replaces the old `patchKVMap`. Each per-row UI action now sends a minimal targeted patch: - Edit one row: `{headers: {key: newValue}}` - Delete one row: `{headers_remove: [key]}` - Add one row: `{headers: {newKey: newValue}}` - Convert to secret: `{headers: {key: "${keyring:NAME}"}}` The redacted sentinel for unchanged keys never round-trips, by construction. - KVValueCell.vue — restore the `isBackendRedacted` branch. When the cell renders `***REDACTED***`, the reveal / Convert-to-secret buttons disappear (we don't hold the real value) and the cell shows the sentinel verbatim with a tooltip explaining that editing still works through the inline edit button. macOS Swift: - ServerDetailView.swift::saveEdits — switch from "send the full parsed map" to "diff against `server.headers` / `server.env` and send the diff". New private helper `diffKVMap(original:next:)` returns a `(set, remove)` tuple suitable for the deep-merge PATCH body. The same invariant holds: leaving a redacted line untouched in the textarea produces `next[k] == "***REDACTED***" == original[k]`, so the key stays out of both sides of the diff and the backend preserves the real value. - ServerDetailView.swift::kvRow — restore the `value != "***REDACTED***"` gate on the Convert-to-secret button (the sentinel isn't useful as a keyring payload). - ServerDetailView.swift::editHeaders doc — explain the new flow. End-to-end verification against the live local instance: $ curl -s -H "X-API-Key: ..." /api/v1/servers | jq '...synapbus.headers' → {"Authorization": "***REDACTED***"} # redacted ✓ $ jq '...synapbus.headers' ~/.mcpproxy/mcp_config.json → {"Authorization": "Bearer 1d386..."} # real on disk $ curl -X PATCH .../servers/synapbus -d '{"headers":{"X-Trace-Test":"merge-works"}}' $ jq '...synapbus.headers' ~/.mcpproxy/mcp_config.json → {"Authorization": "Bearer 1d386...", "X-Trace-Test": "merge-works"} # real token preserved ✓ $ curl -X PATCH .../servers/synapbus -d '{"headers_remove":["X-Trace-Test"]}' $ jq '...synapbus.headers' ~/.mcpproxy/mcp_config.json → {"Authorization": "Bearer 1d386..."} # X-Trace-Test deleted ✓ PR #425's E2E tests still pass (TestE2E_PatchDeepMergesEnvAndHeaders, TestE2E_MultipleEnableDisablePreservesConfig — both exercise the MCP tool which still redacts). PR #425's intent is fully preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback on the previous round: REST PATCH had two ways to delete
a key (`headers_remove: ["X"]` array vs MCP tool's `{"X": null}`). The
MCP tool's syntax is cleaner and is a documented standard (RFC 7396).
Unify on it and add a CLI command so all three interfaces — MCP, REST,
CLI — behave the same way.
Backend (internal/httpapi/server.go):
- AddServerRequest.Headers and .Env switch from `map[string]string` to
`map[string]*string`. Go's encoding/json then maps a missing key to
"no entry", a present non-null value to a non-nil `*string`, and a
present `null` to a nil pointer. The merge loop reads each entry: nil
pointer = delete, non-nil = upsert.
- Drop the `headers_remove` / `env_remove` array fields. A single `null`
in the same map carries the same intent and aligns with the MCP tool.
- POST (add) ignores nil entries via the new flattenNullableMap helper;
`null` on create has no meaning.
- redactServerHeaders / SSE redaction unchanged.
Tests (internal/httpapi/patch_server_test.go):
- Rewrite the previous `*_remove`-style tests to use literal JSON null
payloads via `json.RawMessage`. The raw-byte approach is independent
of any Go marshaling quirks that could collapse `null` values.
- New TestHandlePatchServer_HeadersEmptyStringSetsNotDeletes pins the
distinction between `""` (set to empty) and `null` (delete) — JSON
Merge Patch is explicit about it and a future refactor that
"helpfully" collapses one to the other would silently break.
- Total: 7 tests, all green.
Web UI (frontend/src/views/ServerDetail.vue):
- deleteKv now sends `{headers: {key: null}}` instead of the array
form. JSON.stringify emits `null` literally, no special handling.
- Drop the `scopeRemoveKey` helper (no longer needed).
macOS Swift (native/macos/MCPProxy/MCPProxy/Views/ServerDetailView.swift):
- diffKVMap returns a single `[String: Any]` patch dict where deleted
keys map to `NSNull()` instead of returning the previous
`(set, remove)` tuple.
- saveEdits writes the patch as `updates["headers"] = patch` directly;
no `headers_remove` companion field anymore.
- performConvertToSecret sends a single-key patch
`{field: {key: ref}}` instead of building the full map — minimal
wire payload, never round-trips the redacted Authorization.
- The trap was real and surprising: Swift's default `JSONEncoder` on
`[String: String?]` SILENTLY DROPS nil entries from the JSON output.
Using `[String: Any]` with `NSNull()` + `JSONSerialization` (the
encoder our APIClient already uses) renders `null` correctly.
Swift unit test (native/macos/MCPProxy/MCPProxyTests/MergePatchEncodingTests.swift):
- 4 tests pinning the encoding contract:
1. NSNull encodes as literal `null` via JSONSerialization.
2. A delete-only patch round-trips through JSON and the value
parses back as NSNull (not "", not absent).
3. The wrong path — `[String: String?]` + default `JSONEncoder` —
does silently drop nils. Documented as a poison-pill test so a
future refactor that "simplifies" to it has to explicitly delete
this test and read the comment first.
4. Empty string still encodes as `""` and explicit null as `null`
— the JSON Merge Patch set-vs-delete distinction is preserved.
CLI (cmd/mcpproxy/upstream_cmd.go + internal/cliclient/client.go):
- New `mcpproxy upstream patch <name>` subcommand with flags:
--header K=V upsert (repeatable)
--header-remove K delete (repeatable)
--env K=V upsert (repeatable)
--env-remove K delete (repeatable)
- New cliclient.PatchServer(name, body) sends raw JSON to PATCH
/api/v1/servers/{name}. Body shape is the same JSON Merge Patch the
Web UI and macOS tray send.
- Closes the CLI gap I called out in the boundaries-matrix summary —
REST, MCP, and CLI now all support both write and delete on headers
/ env with the same semantics.
Live end-to-end verification:
$ mcpproxy upstream patch synapbus --header "X-Cli-Test: hello-from-cli"
✅ Patched synapbus: 1 header(s) set
→ on disk: { Authorization (real Bearer), X-Cli-Test } (Auth preserved)
$ mcpproxy upstream patch synapbus --header-remove X-Cli-Test
✅ Patched synapbus: 1 header(s) removed
→ on disk: { Authorization (real Bearer) } (Auth preserved)
$ mcpproxy upstream patch synapbus --header "X-Foo: v" --header-remove X-Foo
Error: --header and --header-remove for "X-Foo" conflict; pick one
Web UI: "+ Add header" → X-WebUI-Test=from-browser → Save
→ on disk: { Authorization (real Bearer), X-WebUI-Test }
macOS tray: Edit → textarea pre-populated with
"Authorization=***REDACTED***" → user appends "X-Mac-Test=hello-from-mac"
→ Save
→ on disk: { Authorization (REAL Bearer, preserved!), X-Mac-Test }
PR #463 subagent review confirmed the unrelated "disable tool" pattern
is a different domain (reversible state in BBolt vs destructive
mutation in mcp_config.json) and should not be unified with this work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cted values
User feedback: the Headers card looked split-brained. A non-sensitive
header showed `••••XX (NN chars)` with a Convert-to-secret button. The
Authorization header showed `***REDACTED***` with no button — exactly
the most-likely candidate for "move me to keyring" was the one the UI
refused to convert, because the client side couldn't hand the real value
to the existing two-step (POST /secrets, then PATCH server) flow.
This change unifies both display and conversion:
1. **Single mask format on the wire** — internal/oauth/logging.go
replaces the `***REDACTED***` sentinel in RedactStringHeaders with
MaskValue(v), producing `••••<last2> (<N> chars)` for literal
secrets. The same format the Web UI / macOS tray have been computing
client-side. Bare ${keyring:NAME} / ${env:VAR} references pass
through unchanged (they're labels, not secrets; the UI needs to
recognise them to render the keyring chip).
2. **Server-side atomic convert** — new endpoint:
POST /api/v1/servers/{name}/config-to-secret
body: {"scope": "header"|"env", "key": "<k>", "secret_name": "<n>"}
The backend reads the real value from the loaded config, stores it
in the OS keyring under secret_name, and rewrites the config field
with ${keyring:<n>}. The client never has to possess the plaintext,
so Convert-to-secret now works on the redacted-on-read path too.
3. **Reveal button removed from KVValueCell.vue** — with all literals
displayed identically and Convert-to-secret available everywhere,
the reveal toggle was a security-shaped speed bump with no real use
case. The two paths to peek at a value (open the config file, or
edit-cancel) remain. revealedKeys reactive state and the
reveal/hide events disappear from the parent too.
4. **Symmetric macOS Swift cleanup** — ServerDetailView.swift::kvRow
drops the `value != "***REDACTED***"` gate on the Convert-to-secret
button. maskedHeaderValue() drops the sentinel special case.
performConvertToSecret() calls the new atomic endpoint via the new
APIClient.convertConfigToSecret helper instead of two-stepping
through storeSecret + PATCH.
Backend tests updated for the new format:
- internal/oauth/logging_test.go::TestRedactStringHeaders — asserts the
••••et (40 chars) mask shape with length + last-2 suffix. New
sub-tests pin the keyring/env-ref pass-through, short-value (<=4
chars -> bare ••••), and empty-value ((empty)) edge cases.
- internal/runtime/event_bus_payload_test.go — SSE redaction test
asserts the new format on the wire.
- internal/server/e2e_test.go — both PR #425 round-trip tests now
assert mask shape instead of literal sentinel.
New backend test coverage:
- internal/httpapi/patch_server_test.go — 9 new sub-tests for the
/config-to-secret endpoint validation paths: missing scope / key /
secret_name, invalid scope, key not on server, value already a
reference (keyring + env separately), empty value, server not found.
Happy-path lives in the live verification because secret.Resolver is
a concrete struct without a mock.
End-to-end verification on live local mcpproxy:
GET /api/v1/servers
-> synapbus.headers.Authorization = ••••59 (71 chars) # new format
-> kaggle.headers.Authorization = ••••N} (30 chars) # Bearer\${k...} also masked
POST /api/v1/servers/synapbus/config-to-secret
{"scope":"header","key":"Authorization","secret_name":"synapbus-authorization"}
-> 200 {"reference":"\${keyring:synapbus-authorization}"}
-> on disk: {"Authorization": "\${keyring:synapbus-authorization}"}
-> GET response: passes the bare reference through unchanged
Web UI: Headers row transformed from
`Authorization ••••59 (71 chars) [lock] Convert to secret`
to
`Authorization [key-chip] stored in keyring: synapbus-authorization`
via the Convert-to-secret modal — no intermediate steps for
the user, no plaintext on the client.
CLI: `upstream patch synapbus --header X-Cli-Verify=hello` /
`upstream patch synapbus --header-remove X-Cli-Verify` still
work; the keyring reference on Authorization survives both.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ffccc8d to
fc4051f
Compare
…points
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.
staticcheck ST1005 — error strings must not end with punctuation. Flatten the multi-line message into a single sentence so the no-trailing-period rule is easy to keep over time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
electrolobzik
added a commit
to HaloCollar/mcpproxy-go
that referenced
this pull request
May 16, 2026
Brings in upstream's 9 commits since 2b9b5f9: - 0597762 fix(upstream): stop misclassifying transport errors as auth failures (smart-mcp-proxy#464) - aaec117 fix(diagnostics): correct bug-report URL (smart-mcp-proxy#465) - d27fa38 feat(server-detail): display + edit headers/env, with reveal & convert-to-secret (smart-mcp-proxy#466) - c086770 feat(webui): per-tool enable/disable + bulk Enable All/Disable All (smart-mcp-proxy#463) - 0ef75a8 chore(docs): remove stale root-level docs - 4b4b62a fix(ui): respect engaged flag in sidebar Setup pulse (smart-mcp-proxy#462) - 24aab3d docs(installation): add migrating-from-manual-install section (smart-mcp-proxy#459) - 9b79254 feat(doctor): surface snap-docker override hint when host needs it (smart-mcp-proxy#460) - be927b6 packaging(deb): ship unattended-upgrades whitelist so installs auto-update (smart-mcp-proxy#458) Conflicts resolved: 1. internal/management/service_test.go — purely additive on upstream's side (3 new headers/env t.Run blocks). Halo-main had no overlapping test work. Took upstream's additions verbatim. 2. oas/docs.go + oas/swagger.yaml — auto-generated by swaggo from Go annotations in source. Took --theirs on the conflict then regenerated with `make swagger` against the merged source so both halo-main- and upstream-introduced REST endpoints are reflected. Note on upstream's c086770 (per-tool enable/disable, upstream's version of smart-mcp-proxy#463): halo-main already has its own per-tool enable/disable work (via feat/per-tool-enable-disable + the security-hardening stack on top — admin gating, isToolCallable fail-closed, sentinel-error quarantine synthesis, etc.). The upstream version produced no merge conflicts because the file-level diffs aligned cleanly — the fork's hardening sits atop the same surface upstream landed. Sanity-checked: - `go build ./...` succeeds. - `go test -short ./internal/management/ ./internal/runtime/ ./internal/httpapi/ ./internal/storage/ ./cmd/generate-types/` passes. - internal/server pre-existing sandbox-environment flake (TestBinaryAPIEndpoints/GET_/servers) was verified to fail on pre-merge halo-main (ad31fde) too — not a regression.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Server detail screens (Web UI and macOS tray) now display, edit, and round-trip HTTP headers and environment variables. Includes a click-to-reveal affordance for redacted values and a one-step "Convert to secret" flow that stores the literal value in the OS keyring and rewrites the field as
${keyring:<name>}.Triggered by a real user report: synapbus configured with a static
Authorization: Bearer ...header showed no headers section at all on the Config tab. Diagnosis (trace) revealed a 4-layer drop:internal/runtime/runtime.goGetAllServersemittedargs/working_dirbut notheaders/envinternal/management/service.goListServershad no extractor forheaders/envfrontend/src/views/ServerDetail.vuenative/macos/MCPProxy/MCPProxy/API/Models.swift+ServerDetailView.swiftServerStatusmodel had neither field; edit form's env textarea was a known stub (L939: "env vars not in ServerStatus model, would need config API")What's in the change
Backend wiring (1st commit)
runtime.go::GetAllServersemitsserverStatus.Config.Headersand.Envwhen non-empty.service.go::ListServersaccepts both typedmap[string]string(in-process StateView) and genericmap[string]interface{}(JSON round-trip) shapes for both fields, with three new unit tests.redactServerHeadersinhttpapi/server.go) still applies; thereveal_secret_headers: trueconfig flag continues to govern whether sensitive header values are sent in plaintext or as***REDACTED***.Web UI (1st + 3rd commit)
POST /api/v1/secretswith a suggested name, thenPATCH /api/v1/servers/{id}replacing the value with${keyring:<name>}.KVValueCell.vueencapsulates per-row UX so headers and env share the same display/edit/reveal/convert logic.${keyring:NAME}and${env:VAR}references render as info chips with no reveal/convert (already pointers, not secrets).api.ts: newpatchServer()andstoreSecret()helpers wrapping the existing backend endpoints.types/api.ts: addheadersfield toServerinterface (the contracts.ts twin already had it; api.ts had drifted).macOS Swift (2nd commit)
ServerStatusgainsheadersandenvCodable fields with properCodingKeys.KEY=VALUE per linetextarea parallel to the existing env textarea.editEnvVarsnow pre-populates fromserver.envinstead of starting empty — closes the long-standing stub at L939.saveEdits()sends both maps unconditionally so deletes round-trip; refuses to save if any header value is still***REDACTED***so the redaction sentinel can't silently overwrite a real secret.parseKVTextarea(shared between env + headers) andmaskedHeaderValue(recognises${keyring:…}/${env:…}references, masks literal values, surfaces the redaction sentinel verbatim).KVValueCellis a separate UX refactor.Reveal semantics
The backend continues to redact via
redactServerHeadersunlessreveal_secret_headers: trueis set. TheKVValueCelldetects the***REDACTED***sentinel and disables both reveal and the "Convert to secret" button (the convert flow needs the real value to write to keyring), surfacing a tooltip pointing the user at the config flag. The Swift edit form refuses to save when the textarea still contains***REDACTED***for the same reason.Verification
Web UI — verified end-to-end against a fresh mcpproxy on
:18091with a synapbus-style HTTP server (Bearer token + custom header) and a stdio server with one literal env value and one${keyring:db-url}reference:••••59 (71 chars)redacted; X-Trace••••.Bearer 1d386f72...token, eye becomes hide (🙈).${keyring:NAME}, secret name auto-suggestedsynapbus-demo-authorization, live previewWill be referenced as ${keyring:synapbus-demo-authorization}.demo-stdio:API_KEYandLOG_LEVELshow as redacted with all four actions;DATABASE_URLcorrectly detected as a${keyring:db-url}reference and rendered as an info badge with no reveal / no convert (only edit + delete).Screenshots are embedded in the PR conversation thread.
macOS Swift —
swiftcbuild is clean (only pre-existingSecretsView.swift/MCPProxyApp.swiftwarnings unrelated to this change). I did not replace/Applications/mcpproxy.app/Contents/MacOS/MCPProxybecause the user's tray is actively connected to live servers; visual verification of the macOS surface should happen by pulling this branch, runningmake build, and rebuilding the Swift bundle per the CLAUDE.md instructions.Test plan
go test ./internal/management/ -run TestListServers -v— 3 new subtests pass.go test ./internal/upstream/core/ ./internal/management/ ./internal/httpapi/— green.vue-tsc --noEmit— clean.vite build— bundles clean.make build+ Swiftswiftcrebuild — both green.Out of scope / follow-ups
{type: "keyring"}only. A future iteration could add${env:VAR}reference creation too.🤖 Generated with Claude Code