Skip to content

fix(mcp): detect live tool catalog changes on connected servers - #1771

Open
Aaronontheweb wants to merge 1 commit into
devfrom
fix/1769-mcp-catalog-refresh
Open

fix(mcp): detect live tool catalog changes on connected servers#1771
Aaronontheweb wants to merge 1 commit into
devfrom
fix/1769-mcp-catalog-refresh

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

What

Closes #1769. The daemon enumerated an MCP server's tool catalog exactly once per client lifetime — inside McpClientRuntime.InitializeAsync, only reached at connect/reconnect. A server that added, removed, renamed, or edited tools mid-session stayed invisible to the model until a disconnect + reconnect.

This reuses the reconnection service's existing 30s health tick to also re-list healthy servers' catalogs on the live client (no client rebuild, no OAuth re-run), throttled to once per 5 minutes per server.

How

  • McpClientManager.TryRefreshCatalogAsync(serverName, ct) — re-lists tools via a new IMcpClientRuntime.ListToolsAsync seam, computes a SHA-256 content fingerprint over the sorted, schema-canonicalized tool surface (name, description, input schema, return schema), and republishes McpServerSnapshot with Generation + 1 + a ToolRegistry update only when the fingerprint changed. Same "connection first, tools second" publish ordering as the connect path.
  • McpReconnectionService — the 30s tick's connected branch calls TryRefreshCatalogAsync. Cadence/throttle lives in the manager (McpServerLifecycle.TryClaimCatalogRefresh), so the service stays dumb.
  • IMcpReconnectable.TryRefreshCatalogAsync — new interface method so the service can reach the refresh through its existing abstraction.

Safety invariants

  • A failed refresh never empties the catalog. Last-good snapshot + generation stay published; the throttle claim is rolled back so the next 30s tick retries (no 5-minute blackout after a transient error).
  • A refresh that returns zero tools from a previously non-empty server is treated as transient — the wipe is refused, last-good tools stay, retry on next window.
  • Per-refresh 15s timeout (CatalogRefreshTimeout) — one hung server cannot stall the poll loop, block a reconnect on the per-server gate, or delay daemon shutdown.
  • No-change short-circuit — the fingerprint is order- and key-order-stable, so a server reordering its catalog or schema keys does not churn the index.

Design notes

  • Poll-only by design: nothing in the fleet sends notifications/tools/list_changed, so the notification path was deliberately omitted (fewest moving parts). The refresh mechanism is the exact seam prompts/resources support will hang off later.
  • The connect path marks the catalog fresh, so no redundant re-list ~35s after startup.
  • Shadow catalog disk files remain startup-only — consistent with existing reconnect behavior, not a regression.

Tests

New McpCatalogRefreshTests (10 tests): change republishes generation; no-change doesn't bump; throttle within interval; failed refresh keeps last good; failed refresh rolls back throttle so next tick retries; empty catalog keeps last good; unknown server no-op; fingerprint order-stability; fingerprint changes on description/tool-add; canonical schema ignores key order/whitespace.

All 144 MCP tests in Netclaw.Daemon.Tests pass. Full diff reviewed by three adversarial sub-agents (concurrency, security, simplification) — their findings (throttle leak, unbounded gate hold, empty-catalog wipe, schema churn, dead code) are all addressed in this commit.

The daemon enumerated an MCP server's tool catalog exactly once per
client lifetime (at connect/reconnect). A server that added, removed,
or changed tools mid-session stayed invisible to the model until a
disconnect + reconnect.

Reuse the reconnection service's 30s health tick to also re-list
healthy servers' catalogs on the live client, throttled to once per
5 minutes per server. A SHA-256 content fingerprint over the sorted,
schema-canonicalized tool surface (name, description, input schema,
return schema) detects any add, remove, rename, or schema edit
without a reconnect. A changed catalog republishes the snapshot with
a bumped generation and re-publishes the tool registry; a failed or
empty refresh keeps the last good catalog (never wipes the index).

The re-list is bounded by a 15s per-refresh timeout so one hung
server cannot stall the poll loop, block a reconnect on the per-server
gate, or delay shutdown. A failed refresh rolls back its throttle
claim so the next 30s tick retries rather than waiting out the full
interval.

Addresses #1769.
Comment on lines +331 to +341
catch (Exception ex)
{
// Invariant: a failed refresh must never empty the catalog. Roll back the
// throttle claim so the next 30s tick retries instead of waiting 5 minutes.
lifecycle.RollbackCatalogRefreshClaim(previousRefreshMs);
_logger.LogWarning(ex,
"MCP server '{Name}' catalog refresh failed; keeping generation {Generation} unchanged",
current.Name.Value,
current.Generation);
return false;
}
Comment on lines +154 to +161
catch (Exception ex)
{
// A refresh failure is not a connection failure — the manager keeps the
// last good catalog and the next tick retries. Log and move on.
_logger.LogDebug(ex,
"MCP server '{Name}' catalog refresh threw an exception",
serverName.Value);
}
@Aaronontheweb Aaronontheweb added bug Something isn't working mcp Model context protocol server / client issues. reliability Retries, resilience, graceful degradation labels Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working mcp Model context protocol server / client issues. reliability Retries, resilience, graceful degradation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP client never detects live catalog changes (no list_changed handling; tools only enumerated at connect/reconnect)

1 participant