Skip to content

mcpx: takeover orphans a removed server's upstream HTTP session — never DELETEd, left to expire upstream #260

Closed

Description

@dezren39

Summary

In this repo's pkgs/mcpx copy, a daemon takeover (mcpx upgrade / mcpx daemon --takeover) orphans the upstream streamable-HTTP session of any server that was removed from the configuration between the old and new daemon: the successor never sends the session-termination DELETE, so the session is left to expire on the upstream on its own. pkgs/mcpx/TODO.md line 6 records this as known-not-done.

This is the same defect that was filed as dezren39/darwin-nix#390 and fixed there by dezren39/nix PR #391 (merge 723e091); this repo's mcpx copy does not have that fix.

Evidence (verified on main 54c223c)

  • pkgs/mcpx/TODO.md:6 — "An upstream session whose server is removed from the configuration during a takeover is not adopted and expires upstream on its own; it is not DELETEd."
  • pkgs/mcpx/internal/pool/handoff.go (pool.Adopt, ~line 193): HTTP instances are explicitly skipped — if h.Stdio == nil { if h.HTTP != nil { continue } } — so a handed HTTP session produces no Adopted at all.
  • pkgs/mcpx/internal/daemon/registry_handoff.go (Registry.Adopt): AdoptSessions is only called for pools that match the new configuration; for an unmatched pool only the stdio children become orphans, and the HTTP session is dropped on the floor. Adoption.Attach therefore has nothing to close for it.
  • pkgs/mcpx/internal/mcpclient/http.go:588 — HTTPHandoff carries only SessionID + Negotiated: no endpoint URL or headers, so a successor could not construct a transport for the orphaned session even if it wanted to terminate it. PoolHandoff (pool/handoff.go:38) likewise has no server name for logging.
  • grep -rn TerminateHTTPSession pkgs/mcpx — no matches: the termination helper does not exist in this copy.
  • The primitive does exist: HTTPTransport.Close() (mcpclient/http.go:566) already sends exactly one DELETE for its session id — it is simply never invoked for a removed server's session, because no transport is ever built for it.

Why it matters

Streamable HTTP upstreams hold per-session state (auth context, subscriptions, server-side resources). A config edit that removes a server followed by a routine mcpx upgrade silently leaks that session until the upstream's own expiry, and the leak is invisible — nothing is logged. The stdio side of the same handoff handles orphans correctly (children are closed post-commit), so the HTTP side is an asymmetric gap in an otherwise careful protocol.

Suggested fix (reference: dezren39/nix PR #391)

  1. Carry the endpoint in the handoff: add URL + Headers to mcpclient.HTTPHandoff (populated from the transport at Detach time; the handoff travels only over the takeover unix socket), and add a Server name to pool.PoolHandoff.
  2. In Registry.Adopt, record the HTTP sessions of pools with no match as orphan sessions (no upstream contact at adopt time).
  3. In Adoption.Attach (post-commit only), end each orphan session with exactly one DELETE via a pool.TerminateOrphanedSession helper built on mcpclient's session termination; Abort must leave the predecessor's session untouched. Log success/failure (e.g. upstream.session.terminated / upstream.session.orphaned with server + session + reason).
  4. Remove the TODO.md line 6 entry.

Acceptance criteria

  • A daemon-level regression test (predecessor hands off a pool whose server is absent from the successor's config, with a live HTTP session) shows exactly one DELETE with that session id reaching the upstream after the handoff commits — and zero before the fix.
  • An aborted handoff sends no DELETE (the predecessor keeps its session).
  • A matched pool's sessions are adopted as today (no behaviour change; existing takeover/handoff tests pass).
  • go test ./internal/pool/ ./internal/daemon/ ./internal/mcpclient/ green in pkgs/mcpx.

Activity

  1. added a commit that references this issue on Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions