Skip to content

fix(mcp): preserve live connectors and bound shutdown waiting - #924

Merged
mpfaffenberger merged 1 commit into
mpfaffenberger:mainfrom
breedx:fix/upstream-mcp-shutdown-0827
Sep 10, 2026
Merged

fix(mcp): preserve live connectors and bound shutdown waiting#924
mpfaffenberger merged 1 commit into
mpfaffenberger:mainfrom
breedx:fix/upstream-mcp-shutdown-0827

Conversation

@breedx

@breedx breedx commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Refreshing MCP configuration can replace live connector objects, and stopping a healthy connector can deadlock while cleanup waits for the same registry lock. Preserve existing connectors during refresh and drain registered lifecycle tasks outside that lock, so shutdown has a bounded wait instead of hanging indefinitely.

Problem

  • _initialize_servers() rebuilds already-materialized connectors and resets their status even when only a new configuration needs materializing.
  • stop_server() holds the registry lock while awaiting a lifecycle task whose cleanup needs that lock to unregister itself.
  • Sequential shutdown lets one stuck cleanup delay every other connector.

Change

  • Materialize only previously unseen server IDs; preserve existing object identity and status.
  • Cancel a registered lifecycle task under the lock, then wait outside it for up to five seconds.
  • Do not cancel the cleanup task again on timeout, repeated stop, or cancellation of the caller.
  • Retain registration and report unsuccessful cleanup, including grouped cancellation failures, rather than discarding ownership.
  • Drain registered contexts concurrently and register one stable, deduplicated shutdown callback.
  • Preserve DEBUG exception diagnostics while using generic cleanup warning text.

This is one bounded preservation/shutdown correction in two runtime files. It changes no CLI commands, dependencies, package version, or sync API signatures. The open MCP capability refactor (#834) touches manager documentation, not these lifecycle decisions.

Validation

Base ce1fe372 (v0.0.827), Linux / Python 3.13.13, current upstream lock installed in a dedicated environment: core plugins 0.0.44, Pydantic AI 2.35.0, MCP 1.27.1 and fastmcp-slim 3.4.7 (which supplies the fastmcp client import). The patch adds or changes no dependencies relative to that base. The patch was applied independently to current upstream and requalified; it carries no other pending fixes or integration history.

  • Two behavioral tests on unpatched upstream both fail: healthy stop reaches the cancellation deadline, then old code swallows cancellation and reports success with stale registration; refresh also replaces the existing connector.
  • python -m pytest -q -o addopts= tests/mcp/test_shutdown_preservation.py: 9 passed.
  • Coverage includes healthy and stuck cleanup, caller cancellation, grouped failures, concurrent draining, preserved object/status, hook deduplication and a real local MCP stdio transport closing cleanly.
  • python -m pytest -q -o addopts= tests/mcp tests/agents tests/tools tests/command_line/mcp tests/test_completions_and_small_modules.py tests/test_claude_refresh_review.py: 1886 passed, 7 skipped, 3 warnings, including the newer upstream completion and OAuth regression tests.
  • A separate local combination with optional connector-entry containment passed 1905 tests, 7 skips, 3 warnings. That containment code is not included or required by this PR. Its test helper was imported from the custody suite without collecting unrelated custody tests.
  • Changed-file Ruff lint/format and diff checks pass.

Tests ran with disposable HOME/XDG, no inherited credentials, and blocked socket connect/DNS/bind. The local stdio child uses only the Python standard library; it is not a remote connector or a managed service. Six skips cover unimplemented MCP search; one requires a case-insensitive filesystem. All three coroutine warnings reproduce on the unpatched base, and none was suppressed.

Boundaries

This deliberately does not fix restart/reload/remove ordering, pending startup ownership, late startup registration, manager/CLI completion reporting, or completion-time agent rebinding. Those require separate changes. The deadline bounds how long callers wait; it does not force-terminate a process or prove failed cleanup eventually succeeds. Concurrent stop-all snapshots registered contexts rather than closing a global startup gate. Operations must remain on the owning event loop.

Connector-entry containment is also separate: this patch does not swallow startup failures or replay tool calls. Preserving existing objects during refresh is not an implicit hot-reload of their configuration.

The earlier base's tilde-completion assertion was corrected upstream in 612de2b1; its tests pass in the updated candidate. No release-readiness claim is made here.

Keep materialized connector objects/status during manager refresh. Cancel registered lifecycle tasks under the registry lock, then drain outside it with a five-second deadline. Retain ownership on incomplete cleanup and deduplicate the shutdown callback. Restart ordering and pending startup ownership remain separate concerns.
@breedx

breedx commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

The preservation/shutdown fix now passes all upstream CI checks, including the real local stdio cleanup regression. This gives the narrowly scoped patch a green review baseline without bundling restart sequencing or connector-entry containment.

  • Full macOS test job: 7,826 passed, 14 skipped, 28 warnings; all nine new preservation/shutdown tests passed.
  • Windows encoding checks: passed.
  • Quality checks: passed.
  • Reviewed head remains eb11d855, based on ce1fe372 (0.0.827). No post-review source changes.

The job is labeled Python 3.13 but its test paths show Python 3.14.7; local focused/control qualification used Python 3.13.13. CI warnings include plugin HTTP-client deprecations, deliberately exercised deprecated config APIs, SQLite resource warnings and unawaited test coroutines. They were inspected and not suppressed; only the three local coroutine warnings have independent base controls in this submission evidence. Green checks do not imply warning-free operation, successful cleanup of every remote connector, or release qualification.

Run: https://github.com/mpfaffenberger/code_puppy/actions/runs/34171236267

@breedx

breedx commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

The follow-up restart fix is now proposed in #930 as a dependent draft. It relies on this PR’s unlocked, bounded lifecycle stop before replacing a connector and rebinding the requesting agent. Your lifecycle patch alone still reproduces the premature-rebind command control; the combined candidate passes the restart regressions. #930 must not merge first.

@mpfaffenberger
mpfaffenberger merged commit 4555c37 into mpfaffenberger:main Sep 10, 2026
3 checks passed
@breedx

breedx commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Resubmitted as #932 after revert #931. GitHub cannot reopen a merged PR, so the replacement is a fresh single-commit application on current main. The patch is unchanged: matching stable patch IDs and byte-identical changed files verified. All nine focused regressions pass; broader checks are running. The dependent draft #930 now points to #932.

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