From f759118f1c205b922cf7a5e5352cb1be1ad755cd Mon Sep 17 00:00:00 2001 From: subhash polisetti Date: Thu, 22 Oct 2026 17:40:32 -0700 Subject: [PATCH] Notify resources list_changed when removing a resource template McpAsyncServer#removeResourceTemplate removed the template but never sent notifications/resources/list_changed, unlike addResourceTemplate and removeResource. Send it when a template was removed and the server declares resources.listChanged. --- .../server/McpAsyncServer.java | 4 +++ ...erivedCapabilitiesRuntimeChangesTests.java | 1 + .../ResourceTemplateManagementTests.java | 34 +++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 91f973f18..32e76264e 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -723,6 +723,10 @@ public Mono removeResourceTemplate(String uriTemplate) { McpServerFeatures.AsyncResourceTemplateSpecification removed = this.resourceTemplates.remove(uriTemplate); if (removed != null) { logger.debug("Removed resource template: {}", uriTemplate); + if (Boolean.TRUE.equals(this.serverCapabilities.resources().listChanged())) { + return notifyResourcesListChanged(); + } + return Mono.empty(); } else { logger.warn("Failed to remove a resource template with URI '{}' (not found)", uriTemplate); diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/DerivedCapabilitiesRuntimeChangesTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/DerivedCapabilitiesRuntimeChangesTests.java index 17d164ad6..5edde2c02 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/DerivedCapabilitiesRuntimeChangesTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/DerivedCapabilitiesRuntimeChangesTests.java @@ -78,6 +78,7 @@ void addAndRemoveResourceAndTemplateWithDerivedCapabilities() { StepVerifier.create(server.addResource(asyncResource("test://added"))).verifyComplete(); StepVerifier.create(server.removeResource("test://initial")).verifyComplete(); StepVerifier.create(server.addResourceTemplate(asyncTemplate("test://items/{id}"))).verifyComplete(); + StepVerifier.create(server.removeResourceTemplate("test://items/{id}")).verifyComplete(); assertThat(transport.getAllSentMessages()).isEmpty(); } diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/ResourceTemplateManagementTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/ResourceTemplateManagementTests.java index eaf3d41a3..927398441 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/ResourceTemplateManagementTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/ResourceTemplateManagementTests.java @@ -9,6 +9,8 @@ import io.modelcontextprotocol.MockMcpServerTransport; import io.modelcontextprotocol.MockMcpServerTransportProvider; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate; import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; @@ -146,6 +148,38 @@ void testRemoveNonexistentResourceTemplate() { StepVerifier.create(mcpAsyncServer.removeResourceTemplate("nonexistent://template/{id}")).verifyComplete(); } + @Test + void testRemoveResourceTemplateNotifiesListChanged() { + ResourceTemplate template = ResourceTemplate.builder(TEST_TEMPLATE_URI, TEST_TEMPLATE_NAME).build(); + + McpServerFeatures.AsyncResourceTemplateSpecification specification = new McpServerFeatures.AsyncResourceTemplateSpecification( + template, (exchange, req) -> Mono.just(ReadResourceResult.builder(List.of()).build())); + + mcpAsyncServer = McpServer.async(mockTransportProvider) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().resources(false, true).build()) + .resourceTemplates(specification) + .build(); + + StepVerifier.create(mcpAsyncServer.removeResourceTemplate(TEST_TEMPLATE_URI)).verifyComplete(); + + assertThat(mockTransportProvider.getTransport().getAllSentMessages()).singleElement() + .isInstanceOfSatisfying(JSONRPCNotification.class, notification -> assertThat(notification.method()) + .isEqualTo(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED)); + } + + @Test + void testRemoveNonexistentResourceTemplateDoesNotNotify() { + mcpAsyncServer = McpServer.async(mockTransportProvider) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().resources(false, true).build()) + .build(); + + StepVerifier.create(mcpAsyncServer.removeResourceTemplate("nonexistent://template/{id}")).verifyComplete(); + + assertThat(mockTransportProvider.getTransport().getAllSentMessages()).isEmpty(); + } + @Test void testReplaceExistingResourceTemplate() { ResourceTemplate originalTemplate = ResourceTemplate.builder(TEST_TEMPLATE_URI, TEST_TEMPLATE_NAME)