Skip to content

MCP server: TryGetResourceToolMap cache never hits due to SelectedAppHostPath mismatch, causing perpetual tools/list refresh loop #14538

@mitchdenny

Description

@mitchdenny

Description

After the fix in #14494 (which removed notification sending from HandleListToolsAsync), the MCP server still enters an infinite tools/listtools/list_changed loop under certain conditions. The fix in #14494 addressed one notification path, but a second root cause remains: the TryGetResourceToolMap cache never returns a hit, causing every tools/list request to trigger a full RefreshResourceToolMapAsync.

Root Cause

In McpResourceToolRefreshService.TryGetResourceToolMap() (line 38):

if (_invalidated || _selectedAppHostPath != _auxiliaryBackchannelMonitor.SelectedAppHostPath)
  • After RefreshResourceToolMapAsync completes, _selectedAppHostPath is set to the connected AppHost's actual path (e.g., /path/to/AppHost.csproj) via connection.AppHostInfo?.AppHostPath.
  • But _auxiliaryBackchannelMonitor.SelectedAppHostPath is always null unless the user explicitly calls the select_apphost tool — it is only set by SelectAppHostTool.
  • So null != "/path/to/AppHost.csproj"always returns false → the cache is never used.

This means every tools/list call triggers RefreshResourceToolMapAsyncGetSelectedConnectionAsyncScanAsync, doing redundant work and generating log noise. Combined with the client detecting changes in the response (e.g., due to resource tool oscillation or JSON ordering differences), this creates an infinite loop.

Evidence

Log analysis from ~/.copilot/logs shows:

  • 60GB and 58GB log files from a single ~4 hour session
  • ~3,800 tool refresh cycles per 3.5 seconds (84% of all log lines are refresh-related)
  • The loop starts immediately on MCP server initialization and never stops
  • Pattern: tools/list_changed notification → client calls tools/list → handler refreshes (cache miss) → handler completes → another tools/list_changed → repeat

Proposed Fix

Change line 38 to compare _selectedAppHostPath against the effective selected connection (which applies the full selection logic: explicit > in-scope > fallback), not the explicit-only SelectedAppHostPath:

// Before (always mismatches when no explicit selection):
if (_invalidated || _selectedAppHostPath != _auxiliaryBackchannelMonitor.SelectedAppHostPath)

// After (compares against the actual connection that would be used):
if (_invalidated || _selectedAppHostPath != _auxiliaryBackchannelMonitor.SelectedConnection?.AppHostInfo?.AppHostPath)

This is a one-line fix that ensures the cache is used when the effective connection has not changed.

Environment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions