Skip to content

Fix MCP server tools/list infinite loop caused by notification race condition#14376

Open
Copilot wants to merge 4 commits intomainfrom
copilot/fix-tools-list-infinite-loop
Open

Fix MCP server tools/list infinite loop caused by notification race condition#14376
Copilot wants to merge 4 commits intomainfrom
copilot/fix-tools-list-infinite-loop

Conversation

Copy link
Contributor

Copilot AI commented Feb 6, 2026

Description

HandleListToolsAsync was sending tools/list_changed notifications as a side effect of handling tools/list requests. When container MCP tools (e.g., db-mcp) oscillated between available/unavailable during enumeration, this created a tight feedback loop: tools/listtools/list_changedtools/list → … (~1,742 calls in ~2m44s until the client killed the server).

Changes:

  • Remove notification from list handlerHandleListToolsAsync no longer sends tools/list_changed. The client already receives the fresh list since it initiated the request.
  • Add change detection to RefreshResourceToolMapAsync — Returns (ToolMap, Changed) tuple. Compares old vs new tool key sets via zero-allocation iteration (count check + ContainsKey) before reporting a change.
  • Gate notifications in HandleCallToolAsync — Only sends tools/list_changed when the tool set actually changed during a refresh triggered by a tool call.
  • RefreshToolsTool unchanged semantically — Still always notifies since it's an explicit user action.

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No
  • Does the change require an update in our Aspire docs?
    • Yes
    • No
Original prompt

This section details on the original issue you should resolve

<issue_title>Aspire MCP Server: tools/list infinite loop when db-mcp tools register (tools_changed notification race condition)</issue_title>
<issue_description>## Description

The Aspire MCP server enters an infinite tools/list request loop when the db-mcp container's tools become available. This causes VS Code to spin indefinitely on "list tools" and the Copilot CLI to intermittently fail to surface the db-mcp tools.

Environment

  • Aspire CLI/SDK: 13.2.0-preview.1.26106.2
  • VS Code: Insiders 1.110.0-insider
  • OS: Windows
  • MCP client: VS Code Copilot Chat + GitHub Copilot CLI

Reproduction Steps

  1. Create an Aspire app with a PostgreSQL resource that has a db-mcp sidecar container
  2. Start the AppHost (aspire run --detach)
  3. Connect the Aspire MCP server to VS Code or Copilot CLI
  4. Wait for the db-mcp container to register its tools

Observed Behavior

When the db-mcp container connects and registers its tools, the Aspire MCP server fires a notifications/tools/list_changed notification. The MCP client responds with a tools/list request, which the server handles. However, the server immediately fires another tools/list_changed notification, causing the client to request tools/list again — creating an infinite loop.

From the logs:

  • 1,742 tools/list request handler calls in ~2 minutes 44 seconds
  • 1,740 Tool list changed notifications in the same period
  • The tool count oscillates between 14 (base) and 23 (base + 9 db-mcp tools), suggesting a race condition where the db-mcp tools are alternately visible and invisible during enumeration
  • The loop only terminates when VS Code kills the MCP server process

Log excerpt (showing the tight loop):

2026-02-06 13:55:44.018 [info] Tool list changed, refreshing tools...
2026-02-06 13:55:44.018 [info] Discovered 14 tools
2026-02-06 13:55:44.020 [info] tools/list request handler completed.
2026-02-06 13:55:44.020 [info] tools/list request handler called.
2026-02-06 13:55:44.053 [info] Tool list changed, refreshing tools...
2026-02-06 13:55:44.053 [info] Discovered 23 tools
2026-02-06 13:55:44.054 [info] tools/list request handler completed.
2026-02-06 13:55:44.054 [info] tools/list request handler called.
2026-02-06 13:55:44.069 [info] Tool list changed, refreshing tools...
2026-02-06 13:55:44.069 [info] Discovered 14 tools
... (repeats 1740+ times over ~2m44s)

Termination stack trace:

System.OperationCanceledException: The operation was canceled.
   at System.Threading.CancellationToken.ThrowOperationCanceledException()
   at ModelContextProtocol.McpSessionHandler.SendMessageAsync()
   at ModelContextProtocol.McpSessionHandler.HandleRequest()
   at ModelContextProtocol.McpSessionHandler.HandleMessageAsync()

Additional Observations from Copilot CLI

In the Copilot CLI, the behavior manifests differently:

  • refresh_tools sometimes returns 14 tools, sometimes 23
  • When it returns 23, the db-mcp tool IDs are reported but calling them returns No MCP client found for tool ID or An error occurred invoking
  • The tools_changed_notice is either never delivered to the CLI client, or arrives with stale tool IDs

Expected Behavior

  1. The notifications/tools/list_changed notification should fire once when the db-mcp tools stabilize, not on every tools/list response
  2. The tool list should be consistent — if 23 tools are reported, all 23 should be callable
  3. No infinite loop between tools/list_changedtools/listtools/list_changed

Possible Root Cause

It appears the server is firing tools/list_changed as a side effect of handling the tools/list request itself (perhaps re-enumerating proxied tools from the db-mcp container triggers a change detection). This creates a feedback loop: list → changed → list → changed → ...

A debounce or dirty-flag mechanism on the tools/list_changed notification would likely resolve this.

Attached Logs

Full VS Code MCP log (10,547 lines) is available — happy to attach if needed.</issue_description>

Comments on the Issue (you are @copilot in this section)

@maddymontaquila [MCPbuglisttools.txt](https://github.com/user-attachments/files/25135408/MCPbuglisttools.txt)

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 2 commits February 6, 2026 19:11
…handler and adding change detection

The MCP server was entering an infinite tools/list loop because:
1. HandleListToolsAsync sent tools/list_changed notification after refreshing
2. The client responded with another tools/list request
3. This created a feedback loop: list → changed → list → changed

Fix:
- Remove SendToolsListChangedNotificationAsync from HandleListToolsAsync
  (the client already gets the fresh list since it requested it)
- Add change detection to RefreshResourceToolMapAsync (returns bool Changed)
- Only send tools/list_changed in HandleCallToolAsync when tools actually changed
- RefreshToolsTool always sends notification (explicit user action)

Co-authored-by: maddymontaquila <12660687+maddymontaquila@users.noreply.github.com>
Co-authored-by: maddymontaquila <12660687+maddymontaquila@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix infinite loop in tools/list on db-mcp tools register Fix MCP server tools/list infinite loop caused by notification race condition Feb 6, 2026
Copilot AI requested a review from maddymontaquila February 6, 2026 19:14
@github-actions
Copy link
Contributor

github-actions bot commented Feb 7, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 14376

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 14376"

@sebastienros
Copy link
Member

@maddymontaquila release/13.2 instead? I can handle it for you

@sebastienros sebastienros marked this pull request as ready for review February 13, 2026 16:30
Copilot AI review requested due to automatic review settings February 13, 2026 16:30
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an infinite tools/listnotifications/tools/list_changed feedback loop in the Aspire MCP server by removing notifications from the tools/list request path and adding tool-set change detection to gate when notifications are emitted.

Changes:

  • Stop sending tools/list_changed as a side effect of tools/list handling.
  • Add “changed” detection to resource tool refresh (RefreshResourceToolMapAsync) and only notify on actual tool-set changes during tool calls.
  • Add a regression test ensuring tools/list does not emit tools/list_changed.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/Aspire.Cli.Tests/Commands/AgentMcpCommandTests.cs Adds coverage to ensure tools/list does not trigger tools/list_changed.
src/Aspire.Cli/Commands/AgentMcpCommand.cs Removes notification from list handler; gates notification in call handler based on change detection.
src/Aspire.Cli/Mcp/McpResourceToolRefreshService.cs Returns (ToolMap, Changed) and computes tool-key set differences.
src/Aspire.Cli/Mcp/IMcpResourceToolRefreshService.cs Updates interface contract to return map + change flag.
src/Aspire.Cli/Mcp/Tools/RefreshToolsTool.cs Adapts to new tuple return while preserving explicit refresh behavior.

@sebastienros
Copy link
Member

@copilot apply changes based on the comments in this thread

… channel wait in test

- Replace HashSet.SetEquals with count check + iterate keys + ContainsKey to
  avoid allocation under lock in McpResourceToolRefreshService.
- Replace Task.Delay(200) in test with Channel.ReadAsync + CancellationTokenSource
  timeout for more deterministic negative assertion.

Co-authored-by: sebastienros <1165805+sebastienros@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 13, 2026

@copilot apply changes based on the comments in this thread

Applied both review comments in f7b0df4:

  • Replaced HashSet.SetEquals with zero-allocation count + iterate + ContainsKey comparison
  • Replaced Task.Delay(200) in test with bounded Channel.ReadAsync + CancellationTokenSource timeout

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.

Aspire MCP Server: tools/list infinite loop when db-mcp tools register (tools_changed notification race condition)

3 participants