Skip to content

fix: disabling a server removes its tools from search again (regression from #1590) - #1603

Merged
Dumbris merged 6 commits into
mainfrom
fix/disable-server-purges-search
Oct 12, 2026
Merged

Dumbris merged 6 commits into
mainfrom
fix/disable-server-purges-search

Conversation

@Dumbris

@Dumbris Dumbris commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Summary

main has been red in E2E Tests since #1590 (Spec 116 PR1, 43c5dc8). TestE2E_DisableServerRemovesToolsFromSearch fails at e2e_test.go:2903. Disabling the server does remove its tools: Step 3 passes in CI and locally. The failure is the re-enable step. The server comes back as "Ready" with no tools, and nothing reconnects it until the 30 s periodic reconcile or later. Locally on main it failed 2 of 3 runs; it passes on 4cc4bcb.

Root cause

  1. Two owners act on one commit. Since fix: single config commit section with per-server generations (Spec 116 PR1; #1558, #1560) #1590, every config commit publishes to configsvc, and its own worker (runCommitWorkers → Manager.AddServerAt) installs and connects the re-enabled instance. The supervisor reconciles the same publish. Its snapshot still holds the disabled entry, so it plans ActionReconnect. The first leg of that action is DisconnectServer(name), which resolves the client by name, so it disconnected the instance the commit worker was still connecting. (Before fix: single config commit section with per-server generations (Spec 116 PR1; #1558, #1560) #1590 the MCP path published only after the connect had finished, so the reconnect was redundant but harmless.)
  2. Ready over a closed transport. managed.Client.Connect releases mc.mu while it dials. A Disconnect in that window closes the transport and resets the state to Disconnected, but Connect then still ran TransitionTo(Ready). The log shows invalid transition from Disconnected to Ready. The client reported Ready, every ListTools failed with client not connected, and because the client looked connected the supervisor never reconnected it.

Fix

  • supervisor: ActionReconnect no longer disconnects by name. AddServer → Manager.AddServerConfigAt already replaces and disconnects a non-equivalent instance, atomically under the manager lock, using the same config.ConnectionEquivalent predicate that plans the reconnect. An instance that already holds the desired config is kept. Spec 116 PR3 (bug: stale connect/disconnect workers and supervisor actions re-install old server configs after a newer commit #1563) still binds the remaining by-name supervisor actions to (generation, instance).
  • managed: Connect now takes a new connectSeq value (guarded by mc.mu) when it starts, and Disconnect advances it. Phase 4 discards the result of a superseded attempt, success or failure, and returns ErrConnectAborted; it closes the dialed transport only when no newer attempt owns the shared core client. The reconnect helpers (tryReconnect, TryReconnectSync) now tear down through teardownForReconnect, which does the sequence bump, the transport close and the state reset in one mc.mu section, as Disconnect does. Manager.AddServerAt treats ErrConnectAborted as "superseded", not as a failure.

Spec 116's commit model is unchanged.

Tests (each failed before the fix)

  • supervisor/TestReconnect_DoesNotDisconnectTheCommittedInstance: the instance installed by the commit with the desired config must not be disconnected by a stale-snapshot ActionReconnect. TestReconnect_ReplacesAnInstanceWithAnOldConnectionConfig keeps the reconnect's purpose.
  • managed/TestConnect_DisconnectDuringDialDoesNotReportReady: a dial seam injects a Disconnect between the transport connect and phase 4, which reproduces the exact Disconnected → Ready log line. Connect must return ErrConnectAborted, and the next connect must work.
  • managed/TestConnect_DisconnectDuringFailingDialKeepsDisconnected, TestConnect_NewerAttemptSupersedesTheOlderOne, TestConnect_ReconnectTeardownSupersedesADialingAttempt, TestConnect_AttemptAdmittedAfterReconnectTeardownOwnsTheClient and TestReconnectTeardown_ExcludesConnectAdmission cover the supersession cases raised in cross-review. Each fails against the previous round's code.
  • The mock in TestSupervisor_Reconcile_DisableServer now models the manager's replace-on-non-equivalent-config behaviour, which is how a disable reaches a connected instance now.

Verification

  • go test -race -count=20 -run TestE2E_DisableServerRemovesToolsFromSearch ./internal/server/: 20/20 pass (main: 2/3 fail).
  • The E2E lane as e2e-tests.yml runs it, and the CI-shaped -race -tags server lane over internal/server/... internal/runtime/... internal/httpapi/... internal/upstream/...: both pass.
  • golangci-lint v2 (.github/.golangci.yml), bare and --build-tags server: no findings in the touched files.
  • Live daemon (make build, scratch port 19110, trusted-library with 320 tools): disabling via REST and via MCP upstream_servers patch leaves 0 tools in retrieve_tools, describe_tool and REST /index/search, both immediately and after 30 s. Re-enabling restores 20/20 retrieve results, describe_tool and 100 index hits within 3 s, and they are still there after 33 s.

Cross-review

OpenCode gpt-6.1-sol (variant high): rounds 1-4 raised findings (three medium, one low) in the managed-client supersession logic, and all were fixed. Round 5 returned VERDICT: PASS. Round 6 reviewed the Windows-only test-ordering fix and returned VERDICT: PASS.

🤖 Generated with Claude Code

…1590)

TestE2E_DisableServerRemovesToolsFromSearch has failed on main since
Spec 116 PR1 at its re-enable step (e2e_test.go:2903): disable purges the
server's tools correctly, but after re-enable the server stays "Ready"
with no tools.

Root cause. Every config commit now publishes to configsvc, so the
supervisor reconciles a change the commit's own worker is already
applying. Its snapshot still holds the disabled entry, so it plans
ActionReconnect, whose first leg DisconnectServer(name) resolves the
client by name and disconnects the instance the commit worker had just
installed and was connecting. managed.Client.Connect then finished with
TransitionTo(Ready) although that Disconnect had closed the transport and
reset the state, so the client reported Ready over a closed transport:
every ListTools failed with "client not connected" and, since the client
looked connected, nothing reconnected it.

Fix.
- supervisor: ActionReconnect no longer disconnects by name. AddServer
  (Manager.AddServerConfigAt) already replaces and disconnects, atomically
  under the manager lock, an instance whose connection config differs from
  the desired one, using the same ConnectionEquivalent predicate that
  plans the reconnect; an instance already holding the desired config is
  kept. Spec 116 PR3 still binds the remaining by-name supervisor actions
  to (generation, instance).
- managed: Connect returns ErrConnectAborted and stays Disconnected when a
  Disconnect ran during the dial, instead of reporting Ready; the manager
  treats that as superseded, not failed.

Tests (failing first): supervisor
TestReconnect_DoesNotDisconnectTheCommittedInstance, managed
TestConnect_DisconnectDuringDialDoesNotReportReady (reproduces the exact
"invalid transition from Disconnected to Ready" log line via a dial seam).
TestSupervisor_Reconcile_DisableServer's mock now models the manager's
replace-on-non-equivalent-config behaviour.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: eb85784
Status: ✅  Deploy successful!
Preview URL: https://f09bfd1a.mcpproxy-docs.pages.dev
Branch Preview URL: https://fix-disable-server-purges-se.mcpproxy-docs.pages.dev

View logs

… state (review r1)

The abort check looked only at a successful dial finding the state
Disconnected. A Disconnect that cancelled the dial made it fail, and the
failure branch overwrote the reset state with Error/PendingAuth; a newer
Connect that re-entered Connecting (or reached Ready) also defeated the
state check, letting the older attempt publish over it.

Connect now takes a new connectSeq under mc.mu when it starts and every
Disconnect advances it; phase 4 discards the attempt's result, success or
failure, when the sequence moved, and closes the dialed transport only
when no newer attempt owns the shared core client. Tests cover the
failing-dial and newer-attempt cases (both fail on the state-only check).
…review r2)

tryReconnect and TryReconnectSync tear the connection down without mc.mu
and then start their own Connect. An older attempt whose dial finished in
between saw an unchanged connectSeq and published Ready over the closed
transport, so the helper's Connect hit ErrConnectAlreadyActive and the
client stayed Ready over a closed transport. Both helpers now advance
connectSeq under mc.mu before the teardown. Regression test parks an
attempt after its dial and releases it between the helper's teardown and
its replacement Connect (fails without the supersede).
The reconnect helpers bumped connectSeq under mc.mu but then cancelled,
closed the transport and reset the state outside it, so a Connect from
another caller (ConnectAll, the supervisor) admitted in between captured
the new sequence and could publish Ready over the transport the teardown
then closed. teardownForReconnect now does the bump, the transport close
and the reset in one mc.mu section, as Disconnect does, which orders
Connect admission (phase 1, under mc.mu) against the whole teardown.
Adds coverage for an attempt admitted after the teardown.
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: fix/disable-server-purges-search

Available Artifacts

  • archive-darwin-amd64 (31 MB)
  • archive-darwin-arm64 (28 MB)
  • archive-linux-amd64 (19 MB)
  • archive-linux-arm64 (17 MB)
  • archive-windows-amd64 (31 MB)
  • archive-windows-arm64 (28 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (27 MB)
  • installer-dmg-darwin-arm64 (25 MB)
  • smart-mcp-proxymcpproxy-goSHVDEL.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 38295257002 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

… (review r4)

Round 4 noted the round-3 test runs its competing Connect after the
teardown and so also passes with the non-atomic teardown. A seam between
the transport close and the state reset now asserts mc.mu (the lock
Connect admission takes) is held there, for both reconnect helpers; it
fails against the round-3 shape.
…ions

reconcile dispatches its actions asynchronously; on the Windows runner the
initial ActionConnect from the old snapshot ran after the explicit
ActionReconnect and re-installed the old config, failing
TestReconnect_ReplacesAnInstanceWithAnOldConnectionConfig. Wait on
actionWg after each reconcile in both new tests.
@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 76.31579% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/upstream/manager.go 0.00% 5 Missing ⚠️
internal/upstream/managed/client.go 87.87% 1 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

@Dumbris
Dumbris merged commit bc08b3d into main Oct 12, 2026
48 of 52 checks passed
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