Files
David Gageot 63161fea0a feat(tools/lifecycle): extract Supervisor and port mcp.Toolset onto it
Adds lifecycle.Supervisor: a single, well-tested implementation of the
connect / watch / restart / stop dance that pkg/tools/mcp had grown
inline as watchConnection / tryRestart / forceReconnectAndWait. The
supervisor:

  * accepts a transport-agnostic Connector that returns a Session
    (Wait + Close), so MCP, remote MCP, and the upcoming LSP port all
    share the same lifecycle code.
  * has a configurable Policy: Restart (Never/OnFailure/Always),
    MaxAttempts, Backoff (Initial/Max/Multiplier/Jitter), and three
    callback hooks (OnDisconnect, OnRestart, OnFailed). Defaults match
    historical behaviour: RestartOnFailure, 5 attempts, 1s..32s.
  * tracks state via the Tracker added in the previous commit, so
    callers can query State() / IsReady() / Restarted() instead of
    reading internal fields.
  * implements RestartAndWait via a single 'forceRestart' flag and a
    swappable 'restarted' channel, fixing a subtle race where Close()
    on a clean session caused Wait()==nil and the watcher would fall
    through to Failed instead of reconnecting.

mcp.Toolset is rewritten on top of the supervisor:

  * deletes started / stopping / watcherAlive / restarted state
    fields and the watchConnection / tryRestart / forceReconnectAndWait
    methods (~150 lines).
  * the initialize handshake (including the upstream-bug retry for
    'failed to send initialized notification') moves into a small
    clientConnector adapter; the existing mcpClient interface is
    reused as a Session.
  * cache invalidation and tool/prompt refresh happen via
    OnDisconnect / OnRestart hooks rather than open-coded inside the
    watcher loop.
  * forceReconnectAndWait becomes supervisor.RestartAndWait.
  * tests that previously poked at Toolset.started / Toolset.restarted
    / Toolset.mu now go through Toolset.IsStarted() and
    supervisor.Restarted(); a small testhelpers_test.go provides
    newTestToolset(...) and markStartedForTesting() so per-request
    code paths can be exercised without driving a full Connect.

External behaviour is preserved: the same conditions return
errServerUnavailable (now an alias to lifecycle.ErrServerUnavailable),
the same retry budget governs reconnects, and the public Toolset API
(Tools, ListPrompts, GetPrompt, callTool, Start, Stop, all the
Set*Handler methods) is unchanged. All existing tests still pass under
'go test -race'.

Assisted-By: docker-agent
2026-04-28 18:23:41 +02:00

10 lines
245 B
Go

package lifecycle
import "time"
// ExportedBackoffDelay exposes Backoff.delay for tests in the _test
// package.
func ExportedBackoffDelay(b Backoff, attempt int, randFloat func() float64) time.Duration {
return b.delay(attempt, randFloat)
}