Skip to content

fix(serve): stop persisting CLI flag and env overrides into the config file - #1299

Merged
Dumbris merged 6 commits into
mainfrom
claude/confident-panini-c552d5
Sep 18, 2026
Merged

Dumbris merged 6 commits into
mainfrom
claude/confident-panini-c552d5

Conversation

@Dumbris

@Dumbris Dumbris commented Sep 18, 2026

Copy link
Copy Markdown
Member

Summary

mcpproxy serve persisted its config at three sites (auto-generated api_key, first-run telemetry notice, recordStartupOutcome) by marshalling the in-memory config — which already carried every CLI flag override. So serve --listen :0 (or --listen "", now mapped to :0) wrote "listen": ":0" into mcp_config.json, and the next unflagged serve or tray-launched core silently booted in stdio mode. Verified empirically 2026-09-17 with a fresh {"mcpServers":[]} config.

What changed

  • serveConfigSaver (cmd/mcpproxy/main.go): loadConfig now returns (cfg, saver, err). saver.save reads the config file as its base (config.ReadFile, snapshot of the same at startup as fallback) and overlays only what serve owns: the API key it generated itself (into an empty api_key only) and cfg.Telemetry. All three save sites use it; recordStartupOutcome takes the save func as a parameter.
  • config.ReadFile (internal/config/loader.go): DefaultConfig() + loadConfigFile, nothing else. LoadFromFile is not a read — it applies MCPPROXY_* env overrides, copies MCPPROXY_API_KEY into api_key via Validate, creates data_dir, and replaces the process-global registry list — none of which a save may do.
  • config.LoadWithPath: Load() discarded the file it discovered (./mcp_config.json or the home file) and runServer re-derived <data_dir>/mcp_config.json, a possibly different, never-loaded file. The saver and NewServerWithConfigPath now use the file that was actually loaded. Load() keeps its signature.

Because the base is the file rather than the live config, this also stops --read-only, --log-level, --disable-management, --data-dir, and MCPPROXY_* env values from leaking through these three sites, and a late fatal-serve save no longer resurrects a startup-era server list over changes the runtime persisted since.

The --listen "":0 mapping is untouched.

Not fixed here (pre-existing, same class)

Two other persist paths still marshal the live config with overrides: the telemetry service's anonymous-id persist (internal/telemetry/telemetry.go persistConfigLocked, semver builds only) and Runtime.SaveConfiguration (every API-driven config change). A config-level strip would break the UI's legitimate restart-gated listen edits (internal/runtime/restart_gated.go), so that needs a persisted-vs-effective split — follow-up. Also pre-existing: recordStartupOutcome / MaybePrintFirstRunNotice mutate cfg.Telemetry without the telemetry service's mutex.

Tests

cmd/mcpproxy/serve_flag_persistence_test.go drives loadConfig with a cobra command bound to the package globals, then the save sites, and decodes the file as raw JSON:

  • each of the 6 loadConfig flags (incl. --listen :0) + runServer's --read-only/--log-level mutations do not reach the file; generated api_key and telemetry do
  • MCPPROXY_API_KEY and MCPPROXY_LISTEN are never written
  • a late save keeps servers/settings the runtime persisted after startup, and a key rotated later
  • the fallback (file vanished) keeps the file's key
  • legacy teams block survives (loader normalization)
  • without --config, saves land in the discovered ./mcp_config.json, not <data_dir>/mcp_config.json

Every test was watched failing against the old behaviour before its fix.

Verification

  • go test ./cmd/mcpproxy ./internal/config
  • golangci-lint v2.6.2 with .github/.golangci.yml: 0 issues ✅
  • ./scripts/test-api-e2e.sh: 65/65 (one earlier run had 2 unidentified failures, then 5 consecutive clean runs — timing flake)
  • Cross-review opencode run --model github-copilot/gpt-5.6-sol: 5 rounds; rounds 1–4 each surfaced a genuine issue (late-save clobber, env API key, LoadFromFile side effects, legacy teams erasure, discovery-path mismatch), all fixed with a test; round 5 VERDICT: CLEAN.

🤖 Generated with Claude Code

Dumbris and others added 5 commits September 18, 2026 06:08
`serve` saves the config at three sites (auto-generated api_key, first-run
telemetry notice, recordStartupOutcome) and wrote the in-memory cfg, which
already carried every CLI flag override. `serve --listen :0` therefore
wrote `"listen": ":0"` into mcp_config.json and the next unflagged start
(or the tray-launched core) silently booted in stdio mode.

loadConfig now snapshots the file-loaded config before any flag override
and returns a serveConfigSaver; its save() writes that snapshot plus only
the runtime-generated fields (api_key, telemetry). All three save sites
use it, and recordStartupOutcome takes the save func as a parameter. The
snapshot precedes runServer's own overrides too, so --read-only,
--log-level, --disable-management etc. no longer leak either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…PI key

Cross-review round 1 (gpt-5.6-sol): the fatal-serve-error save can fire
hours after startup and would have written the startup-era snapshot over
servers the runtime persisted since; and Validate() copies
MCPPROXY_API_KEY into cfg.APIKey, so every save wrote that secret to disk.

serveConfigSaver.save now re-reads the file as its base (snapshot only if
the file is unreadable), takes api_key from the raw file, and overlays
only the key serve generated itself plus cfg.Telemetry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cross-review round 2 (gpt-5.6-sol): config.LoadFromFile is not a read —
it applies MCPPROXY_* env overrides, copies MCPPROXY_API_KEY into api_key
via Validate, creates data_dir and replaces the process-global registry
list; the fallback path dropped a configured key when the file vanished;
and a startup-generated key overrode a key rotated later via the API.

The base is now DefaultConfig + json.Unmarshal of the file (snapshot of
the same at startup as fallback), and the generated key only fills an
empty api_key. This also stops env overrides leaking on the serve path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cross-review round 3 (gpt-5.6-sol): a bare json.Unmarshal skipped the
read-time normalizations loadConfigFile applies (legacy "teams" →
"server_edition", created stamps), so a serve-time save erased a legacy
teams block. Export config.ReadFile — DefaultConfig + loadConfigFile,
nothing else — and use it as the saver's base so it stays in lockstep
with the loader.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cross-review round 4 (gpt-5.6-sol): without --config, config.Load()
discovers ./mcp_config.json or the home file but discarded the path, and
runServer re-derived <data_dir>/mcp_config.json — a possibly different,
never-loaded file that the merge would read as its base.

Add config.LoadWithPath (Load keeps its signature) reporting the file it
read or created, absolute; the saver carries that path and runServer uses
it for every save and for the runtime's config path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 18, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 384e84b
Status: ✅  Deploy successful!
Preview URL: https://692fbe47.mcpproxy-docs.pages.dev
Branch Preview URL: https://claude-confident-panini-c552.mcpproxy-docs.pages.dev

View logs

TestNoDoorPublishesARawServerLeaf keys config-returning functions by
bare name and requires unanimity; a config.ReadFile that returns
*Config tainted every os.ReadFile call in the scanned trees (flagged
internal/tray/managers.go:loadIcon). All five red CI jobs were this
one test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 34.32836% with 44 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/config/loader.go 0.00% 25 Missing ⚠️
cmd/mcpproxy/main.go 55.00% 18 Missing ⚠️
cmd/mcpproxy/startup_outcome.go 50.00% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: claude/confident-panini-c552d5

Available Artifacts

  • archive-darwin-amd64 (30 MB)
  • archive-darwin-arm64 (27 MB)
  • archive-linux-amd64 (18 MB)
  • archive-linux-arm64 (16 MB)
  • archive-windows-amd64 (30 MB)
  • archive-windows-arm64 (26 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (24 MB)
  • installer-dmg-darwin-arm64 (21 MB)
  • smart-mcp-proxymcpproxy-goX9EU1G.dockerbuild (0 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 35324363616 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@Dumbris
Dumbris merged commit 62bf655 into main Sep 18, 2026
41 checks passed
Dumbris added a commit that referenced this pull request Sep 18, 2026
…gnature

CI on PR #1300 failed to build cmd/mcpproxy on every platform: "assignment
mismatch: 2 variables but loadConfig returns 3 values". Unrelated to this
PR's instance-ID changes -- main itself is currently broken this way (#1299
changed loadConfig to also return a *serveConfigSaver, and the
listen_flag_test.go added by #1301 wasn't updated for it). Discard the
unused saver return to match the real signature.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants