Skip to content

MCPServer: ctx.notify_tools_changed() is a silent no-op on handshake-era (2025-11-25) connections, and tools.listChanged is advertised as false #3862

Open

Description

@dcode

Summary

On a connection opened with the initialize handshake (2025-11-25 and earlier), MCPServer has two problems:

  1. It advertises tools: {listChanged: false}, even though add_tool() / remove_tool() change the list at runtime. MCPServer.run() gives no public way to pass NotificationOptions(tools_changed=True).
  2. Context.notify_tools_changed() publishes only to the subscriptions/listen bus. A handshake-era client gets nothing, and there is no error or warning. notify_prompts_changed() and notify_resources_changed() probably behave the same way (not tested).

The 2026-07-28 path works. So a server written against the documented API updates its tool list for modern clients and silently never does for handshake clients.

Why it matters now

Goose 1.57.0 negotiates 2026-07-28 but never opens subscriptions/listen (aaif-goose/goose#13055). The only way to get a tool-list change to Goose today is the 2025-11-25 era, and this bug drops it there. The Goose side works: with the workaround below, Goose lists and calls a tool added mid-turn.

Workaround we use: await ctx.request_context.session.send_tool_list_changed(), plus serving with create_initialization_options(NotificationOptions(tools_changed=True)).

Reproduction (mcp 2.3.0, Python 3.14)

Both tests fail. Replacing ctx.notify_tools_changed() with ctx.request_context.session.send_tool_list_changed() makes the second pass, which shows the client side is fine.

import anyio
import mcp_types as types
import pytest

from mcp.client.client import Client
from mcp.server.mcpserver import Context, MCPServer

pytestmark = pytest.mark.anyio


def _server() -> MCPServer:
    server = MCPServer("listchanged-handshake")

    def late_tool() -> str:
        """Registered at runtime."""
        return "late"

    @server.tool()
    async def enable(ctx: Context) -> str:
        """Register late_tool and announce it with the public Context API."""
        server.add_tool(late_tool)
        await ctx.notify_tools_changed()
        return "enabled"

    return server


async def test_handshake_client_is_told_tools_list_changed_is_supported() -> None:
    """A server that can change its tools says so to a 2025-11-25 client."""
    async with Client(_server(), mode="legacy") as client:
        assert client.protocol_version == "2025-11-25"
        tools = client.server_capabilities.tools
        assert tools is not None and tools.list_changed is True  # fails: False


async def test_notify_tools_changed_reaches_handshake_client() -> None:
    """ctx.notify_tools_changed() sends tools/list_changed to a 2025-11-25 client."""
    received = anyio.Event()

    async def on_message(message: object) -> None:
        if isinstance(message, types.ToolListChangedNotification):
            received.set()

    async with Client(_server(), mode="legacy", message_handler=on_message) as client:
        await client.call_tool("enable", {})
        with anyio.move_on_after(1):
            await received.wait()
        assert received.is_set(), "notify_tools_changed() sent nothing"  # fails
        assert "late_tool" in {tool.name for tool in (await client.list_tools()).tools}

Expected

On handshake-era connections:

  • Advertise listChanged: true for tools, or at least make it configurable from MCPServer.
  • Have notify_*_changed() send notifications/*/list_changed on the session. That era has no subscription filter, and a server that declared listChanged may send these notifications.

Related


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    spec-2026-07-28Concerns the SDK's implementation of the 2026-07-28 MCP spec revision
    v2Affects the v2 line (2.x on main)
    on Nov 14, 2026
  2. Bensonluo commented on Nov 14, 2026

    @Bensonluo

    Reproduced on mcp 2.3.0 / Python 3.14: both tests fail exactly as described — list_changed is False on a legacy-mode connection, and notify_tools_changed() never reaches the client (the received event never fires). Swapping in ctx.request_context.session.send_tool_list_changed() makes the second test pass, which confirms the send path itself works fine on a handshake-era connection and the gap is specifically in Context.notify_tools_changed().

    The static picture matches too: MCPServer.run_stdio_async calls create_initialization_options() with no notification options, so the lowlevel default ToolsCapability(list_changed=False) wins; and notify_tools_changed() only publishes ToolsListChanged to the subscriptions bus — there is no consumer that bridges it to send_tool_list_changed for legacy sessions, while notify_resource_updated's docstring documents exactly that legacy path for resources.

    Direction-wise, an era-aware fix looks possible without disturbing the subscriptions/listen design from #3626: advertise listChanged: true when tools can change at runtime, and have notify_*_changed() route per negotiated protocol version — legacy send for handshake-era sessions, bus publish for modern ones. Happy to help implement this (with regression tests for the tools case and the prompts/resources siblings) if that direction sounds right and @dcode isn't already planning to take it.

  3. Varshith-Kali commented on Nov 14, 2026

    @Varshith-Kali

    I can take this one. I have reproduced both failures on mcp 2.3.0: list_changed is False on a legacy-mode connection, and ctx.notify_tools_changed() never reaches the client, while session.send_tool_list_changed() does. I will fix the capability advertisement and the notification routing for handshake-era connections, with the two regression tests from the issue, and check whether notify_prompts_changed() and notify_resources_changed() share the same fault. Please assign the issue to me if that works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingspec-2026-07-28Concerns the SDK's implementation of the 2026-07-28 MCP spec revisionv2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions