Commit f952885
committed
feat(headers): unify delete syntax to JSON Merge Patch + add CLI patch
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>1 parent c23e038 commit f952885
9 files changed
Lines changed: 500 additions & 142 deletions
File tree
- cmd/mcpproxy
- frontend/src/views
- internal
- cliclient
- httpapi
- native/macos/MCPProxy
- MCPProxyTests
- MCPProxy/Views
- oas
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
121 | 121 | | |
122 | 122 | | |
123 | 123 | | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
124 | 157 | | |
125 | 158 | | |
126 | 159 | | |
| |||
248 | 281 | | |
249 | 282 | | |
250 | 283 | | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
251 | 290 | | |
252 | 291 | | |
253 | 292 | | |
| |||
272 | 311 | | |
273 | 312 | | |
274 | 313 | | |
| 314 | + | |
275 | 315 | | |
276 | 316 | | |
277 | 317 | | |
| |||
316 | 356 | | |
317 | 357 | | |
318 | 358 | | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
319 | 365 | | |
320 | 366 | | |
321 | 367 | | |
| |||
2006 | 2052 | | |
2007 | 2053 | | |
2008 | 2054 | | |
| 2055 | + | |
| 2056 | + | |
| 2057 | + | |
| 2058 | + | |
| 2059 | + | |
| 2060 | + | |
| 2061 | + | |
| 2062 | + | |
| 2063 | + | |
| 2064 | + | |
| 2065 | + | |
| 2066 | + | |
| 2067 | + | |
| 2068 | + | |
| 2069 | + | |
| 2070 | + | |
| 2071 | + | |
| 2072 | + | |
| 2073 | + | |
| 2074 | + | |
| 2075 | + | |
| 2076 | + | |
| 2077 | + | |
| 2078 | + | |
| 2079 | + | |
| 2080 | + | |
| 2081 | + | |
| 2082 | + | |
| 2083 | + | |
| 2084 | + | |
| 2085 | + | |
| 2086 | + | |
| 2087 | + | |
| 2088 | + | |
| 2089 | + | |
| 2090 | + | |
| 2091 | + | |
| 2092 | + | |
| 2093 | + | |
| 2094 | + | |
| 2095 | + | |
| 2096 | + | |
| 2097 | + | |
| 2098 | + | |
| 2099 | + | |
| 2100 | + | |
| 2101 | + | |
| 2102 | + | |
| 2103 | + | |
| 2104 | + | |
| 2105 | + | |
| 2106 | + | |
| 2107 | + | |
| 2108 | + | |
| 2109 | + | |
| 2110 | + | |
| 2111 | + | |
| 2112 | + | |
| 2113 | + | |
| 2114 | + | |
| 2115 | + | |
| 2116 | + | |
| 2117 | + | |
| 2118 | + | |
| 2119 | + | |
| 2120 | + | |
| 2121 | + | |
| 2122 | + | |
| 2123 | + | |
| 2124 | + | |
| 2125 | + | |
| 2126 | + | |
| 2127 | + | |
| 2128 | + | |
| 2129 | + | |
| 2130 | + | |
| 2131 | + | |
| 2132 | + | |
| 2133 | + | |
| 2134 | + | |
| 2135 | + | |
| 2136 | + | |
| 2137 | + | |
| 2138 | + | |
| 2139 | + | |
| 2140 | + | |
| 2141 | + | |
| 2142 | + | |
| 2143 | + | |
| 2144 | + | |
| 2145 | + | |
| 2146 | + | |
| 2147 | + | |
| 2148 | + | |
| 2149 | + | |
| 2150 | + | |
| 2151 | + | |
| 2152 | + | |
| 2153 | + | |
| 2154 | + | |
| 2155 | + | |
| 2156 | + | |
| 2157 | + | |
| 2158 | + | |
| 2159 | + | |
| 2160 | + | |
| 2161 | + | |
| 2162 | + | |
| 2163 | + | |
| 2164 | + | |
| 2165 | + | |
| 2166 | + | |
| 2167 | + | |
| 2168 | + | |
| 2169 | + | |
| 2170 | + | |
| 2171 | + | |
| 2172 | + | |
| 2173 | + | |
| 2174 | + | |
| 2175 | + | |
| 2176 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2395 | 2395 | | |
2396 | 2396 | | |
2397 | 2397 | | |
2398 | | - | |
2399 | | - | |
2400 | | - | |
2401 | 2398 | | |
2402 | 2399 | | |
2403 | 2400 | | |
2404 | 2401 | | |
2405 | 2402 | | |
2406 | 2403 | | |
| 2404 | + | |
| 2405 | + | |
| 2406 | + | |
| 2407 | + | |
| 2408 | + | |
| 2409 | + | |
| 2410 | + | |
2407 | 2411 | | |
2408 | 2412 | | |
2409 | | - | |
| 2413 | + | |
2410 | 2414 | | |
2411 | 2415 | | |
2412 | 2416 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1224 | 1224 | | |
1225 | 1225 | | |
1226 | 1226 | | |
| 1227 | + | |
| 1228 | + | |
| 1229 | + | |
| 1230 | + | |
| 1231 | + | |
| 1232 | + | |
| 1233 | + | |
| 1234 | + | |
| 1235 | + | |
| 1236 | + | |
| 1237 | + | |
| 1238 | + | |
| 1239 | + | |
| 1240 | + | |
| 1241 | + | |
| 1242 | + | |
| 1243 | + | |
| 1244 | + | |
| 1245 | + | |
| 1246 | + | |
| 1247 | + | |
| 1248 | + | |
| 1249 | + | |
| 1250 | + | |
| 1251 | + | |
| 1252 | + | |
| 1253 | + | |
| 1254 | + | |
| 1255 | + | |
| 1256 | + | |
| 1257 | + | |
| 1258 | + | |
| 1259 | + | |
| 1260 | + | |
| 1261 | + | |
| 1262 | + | |
| 1263 | + | |
| 1264 | + | |
| 1265 | + | |
| 1266 | + | |
| 1267 | + | |
| 1268 | + | |
| 1269 | + | |
| 1270 | + | |
| 1271 | + | |
| 1272 | + | |
| 1273 | + | |
| 1274 | + | |
| 1275 | + | |
| 1276 | + | |
| 1277 | + | |
| 1278 | + | |
| 1279 | + | |
| 1280 | + | |
| 1281 | + | |
| 1282 | + | |
1227 | 1283 | | |
1228 | 1284 | | |
1229 | 1285 | | |
| |||
0 commit comments