Fix: Configuration auto-refresh in-memory sync and SSE event integration#98
Merged
Conversation
This PR fixes critical bugs in the configuration auto-refresh feature that prevented real-time updates in the Web UI Configuration page. ## Changes ### Backend (internal/runtime/) - **lifecycle.go**: Fixed in-memory config sync bug where configSvc path didn't update r.cfg.Servers - Moved mutex-protected update outside if/else to apply to both configSvc and legacy paths - Added comprehensive debug logging (before/after disk save, old vs new server counts) - Added emitConfigSaved() call to broadcast config.saved events - **event_bus.go**: Added emitConfigSaved() method for SSE integration - **events.go**: Added EventTypeConfigSaved constant - **event_bus_test.go**: Unit tests for event bus subscription and emission ### Frontend (frontend/src/) - **stores/system.ts**: Added SSE listener for config.saved events - Dispatches CustomEvent 'mcpproxy:config-saved' to notify Settings view - **views/Settings.vue**: Added event listener and cleanup - Calls loadConfig() when config.saved event received - Proper onMounted/onUnmounted lifecycle hooks ### Testing (internal/) - **testutil/binary.go**: Added GetConfigPath() helper for E2E tests - **server/e2e_config_auto_refresh_test.go**: Integration test structure for config auto-refresh ## Bug Fix Details **Problem**: When adding servers via Web UI, the configuration file was updated but the /api/v1/config endpoint returned stale data, causing Monaco editor to not show new servers. **Root Cause**: In SaveConfiguration(), the in-memory config update only happened in the legacy else branch, not in the configSvc path. **Solution**: Moved the in-memory update outside the conditional to ensure both paths update r.cfg.Servers. ## Verification Tested with Playwright E2E: - ✅ Server added via Web UI appears in config file - ✅ config.saved SSE event received in browser console - ✅ Monaco editor automatically reloads and displays new server - ✅ API endpoint returns fresh data immediately 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Dumbris
added a commit
that referenced
this pull request
Jun 22, 2026
…1076) (#745) * feat(registries): harden registry fetches against SSRF (CWE-918, MCP-1076) Registry server-list fetches use a user-supplied URL (`registry add-source`), which CodeQL flags as go/request-forgery (#97/#98): a malicious or typo'd registry source could point the daemon at internal/metadata endpoints (169.254.169.254, RFC1918 ranges). Add an SSRF guard centered on a single `isBlockedIP` predicate (loopback, RFC1918, RFC6598 CGNAT, link-local incl. the cloud-metadata endpoint, IPv6 unique-local, unspecified, multicast; fails closed), applied at three layers: - dial time: a net.Dialer Control hook on the shared registry HTTP client rejects the actual resolved IP before connect — covers hostnames that resolve into blocked ranges and closes the DNS-rebinding TOCTOU window a parse-time check alone cannot. - fetch pre-flight: validateRegistryURL rejects a literal-IP host (defense in depth + an explicit barrier at the http.NewRequest sink). - add-source/edit-source: literal-IP fail-fast (no DNS, stays pure/offline) so a bad source is refused up front with the stable invalid_registry_url code. Opt out with a new top-level `allow_private_registry_fetch` config flag (default false) for operators who run a trusted registry mirror on an internal/private address. TDD: table tests for every blocked/allowed range (incl. CGNAT and RFC1918 boundaries), an end-to-end loopback dial block, add-source literal-IP rejection, and the two registry-add E2E daemon tests opt in via the new flag. * test(server): opt loopback registry fetch tests past the SSRF guard The in-process server tests that fetch a registry served on a loopback httptest server (consistency_official, mcp_add_from_registry's startTestRegistry helper) now hit the new SSRF dial guard, because the registries-package test bypass does not apply to the server package's test binary. Set AllowPrivateRegistryFetch on their SetRegistriesFromConfig configs — exactly how an operator opts in for a trusted internal mirror — so the fetch is permitted while the production guard stays active for the add-source rejection test. Fixes the linux/arm64 Build Binaries leg (TestCrossSurfaceConsistency_*, TestHandleUpstreamServers_AddFromRegistry_*). * fix(registries): close SSRF proxy-bypass with app-layer host resolution CodexReviewer (round 2) found a real gap: the registry client clones http.DefaultTransport, which keeps Proxy: ProxyFromEnvironment. With HTTP_PROXY/HTTPS_PROXY set, the dialer connects to the PROXY as the first hop, so registryDialControl only ever validates the proxy's IP — never the real target. A hostname resolving to loopback/private/link-local (incl. 169.254.169.254) could still be fetched through a proxy. Add an application-layer guard (guardRegistryTargetHost) that resolves the target host and rejects the fetch if ANY resolved IP is in a blocked range. It runs before the request in registryGet, independent of the transport/proxy, so it holds in the proxy case; the dial-time Control stays as defense-in-depth for the direct path and DNS-rebind TOCTOU. Fail-open on resolver error (a flaky resolver must not break every fetch); literal IPs already covered pre-flight; relaxed by the allow_private_registry_fetch opt-in. Regression coverage: a public-looking hostname resolving to 169.254.169.254 is refused with HTTP(S)_PROXY set; plus direct predicate tests (mixed public+private resolution blocked, all-public allowed, resolver-error fail-open, public literal allowed). Resolver is injectable via a package var for deterministic, network-free tests.
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
Fixes critical bugs in the configuration auto-refresh feature that prevented real-time updates in the Web UI Configuration page.
Problem: When adding servers via Web UI, the configuration file was updated but the
/api/v1/configendpoint returned stale data, causing Monaco editor to not show new servers until manual reload.Root Cause: In
SaveConfiguration(), the in-memory config update only happened in the legacyelsebranch, not in theconfigSvcpath, leading to API responses with outdated data.Changes
Backend Fixes (
internal/runtime/)r.cfg.Servers = latestServersoutside the if/else blockemitConfigSaved()call to broadcastconfig.savedSSE eventsemitConfigSaved()method for SSE integrationEventTypeConfigSavedconstant for config save eventsFrontend Integration (
frontend/src/)config.savedeventsmcpproxy:config-savedto notify Settings viewhandleConfigSaved()event listenerloadConfig()when config.saved event receivedonUnmounted()hookTesting (
internal/)GetConfigPath()helper for E2E testsTest Plan
Manual Testing with Playwright ✅
{payload: Object, timestamp: 1761143174}Results:
config.savedSSE event broadcastEvent Flow Verification
Affected Components
/api/v1/config)Breaking Changes
None. This is a bug fix that ensures existing functionality works as designed.
Backwards Compatibility
✅ Fully backwards compatible
Performance Impact
Minimal overhead:
🤖 Generated with Claude Code