Skip to content

Commit 6a8140d

Browse files
itzjbKehrlann
authored andcommitted
Fix NPE when adding or removing features on a server with derived capabilities
Derived server capabilities leave listChanged null since #928. Compare with Boolean.TRUE.equals in McpAsyncServer so runtime add/remove of tools, resources, resource templates and prompts completes instead of throwing a NullPointerException.
1 parent 5be23a9 commit 6a8140d

2 files changed

Lines changed: 125 additions & 7 deletions

File tree

‎mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ public Mono<Void> addTool(McpServerFeatures.AsyncToolSpecification toolSpecifica
371371
this.tools.add(wrappedToolSpecification);
372372
logger.debug("Added tool handler: {}", wrappedToolSpecification.tool().name());
373373

374-
if (this.serverCapabilities.tools().listChanged()) {
374+
if (Boolean.TRUE.equals(this.serverCapabilities.tools().listChanged())) {
375375
return notifyToolsListChanged();
376376
}
377377
return Mono.empty();
@@ -519,7 +519,7 @@ public Mono<Void> removeTool(String toolName) {
519519
return Mono.defer(() -> {
520520
if (this.tools.removeIf(toolSpecification -> toolSpecification.tool().name().equals(toolName))) {
521521
logger.debug("Removed tool handler: {}", toolName);
522-
if (this.serverCapabilities.tools().listChanged()) {
522+
if (Boolean.TRUE.equals(this.serverCapabilities.tools().listChanged())) {
523523
return notifyToolsListChanged();
524524
}
525525
}
@@ -622,7 +622,7 @@ public Mono<Void> addResource(McpServerFeatures.AsyncResourceSpecification resou
622622
else {
623623
logger.debug("Added resource handler: {}", resourceSpecification.resource().uri());
624624
}
625-
if (this.serverCapabilities.resources().listChanged()) {
625+
if (Boolean.TRUE.equals(this.serverCapabilities.resources().listChanged())) {
626626
return notifyResourcesListChanged();
627627
}
628628
return Mono.empty();
@@ -655,7 +655,7 @@ public Mono<Void> removeResource(String resourceUri) {
655655
McpServerFeatures.AsyncResourceSpecification removed = this.resources.remove(resourceUri);
656656
if (removed != null) {
657657
logger.debug("Removed resource handler: {}", resourceUri);
658-
if (this.serverCapabilities.resources().listChanged()) {
658+
if (Boolean.TRUE.equals(this.serverCapabilities.resources().listChanged())) {
659659
return notifyResourcesListChanged();
660660
}
661661
return Mono.empty();
@@ -691,7 +691,7 @@ public Mono<Void> addResourceTemplate(
691691
logger.debug("Added resource template handler: {}",
692692
resourceTemplateSpecification.resourceTemplate().uriTemplate());
693693
}
694-
if (this.serverCapabilities.resources().listChanged()) {
694+
if (Boolean.TRUE.equals(this.serverCapabilities.resources().listChanged())) {
695695
return notifyResourcesListChanged();
696696
}
697697
return Mono.empty();
@@ -891,7 +891,7 @@ public Mono<Void> addPrompt(McpServerFeatures.AsyncPromptSpecification promptSpe
891891
else {
892892
logger.debug("Added prompt handler: {}", promptSpecification.prompt().name());
893893
}
894-
if (this.serverCapabilities.prompts().listChanged()) {
894+
if (Boolean.TRUE.equals(this.serverCapabilities.prompts().listChanged())) {
895895
return this.notifyPromptsListChanged();
896896
}
897897

@@ -925,7 +925,7 @@ public Mono<Void> removePrompt(String promptName) {
925925

926926
if (removed != null) {
927927
logger.debug("Removed prompt handler: {}", promptName);
928-
if (this.serverCapabilities.prompts().listChanged()) {
928+
if (Boolean.TRUE.equals(this.serverCapabilities.prompts().listChanged())) {
929929
return this.notifyPromptsListChanged();
930930
}
931931
return Mono.empty();
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.server;
6+
7+
import io.modelcontextprotocol.MockMcpServerTransport;
8+
import io.modelcontextprotocol.MockMcpServerTransportProvider;
9+
import io.modelcontextprotocol.spec.McpSchema;
10+
import org.junit.jupiter.api.Test;
11+
import reactor.core.publisher.Mono;
12+
import reactor.test.StepVerifier;
13+
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
import static org.assertj.core.api.Assertions.assertThatCode;
16+
17+
/**
18+
* Runtime add/remove operations on a server whose capabilities were derived from the
19+
* features registered at build time (no explicit {@code capabilities(...)}). The derived
20+
* capabilities leave {@code listChanged} unset, so no list_changed notification is
21+
* expected and the operations must complete normally.
22+
*/
23+
class DerivedCapabilitiesRuntimeChangesTests {
24+
25+
private static final McpSchema.Implementation SERVER_INFO = McpSchema.Implementation.builder("test-server", "1.0.0")
26+
.build();
27+
28+
private static McpServerFeatures.AsyncToolSpecification asyncTool(String name) {
29+
return McpServerFeatures.AsyncToolSpecification.builder()
30+
.tool(McpSchema.Tool.builder(name).description(name).build())
31+
.callHandler((exchange, request) -> Mono.just(McpSchema.CallToolResult.builder().build()))
32+
.build();
33+
}
34+
35+
private static McpServerFeatures.AsyncResourceSpecification asyncResource(String uri) {
36+
return new McpServerFeatures.AsyncResourceSpecification(McpSchema.Resource.builder(uri, uri).build(),
37+
(exchange, request) -> Mono.empty());
38+
}
39+
40+
private static McpServerFeatures.AsyncResourceTemplateSpecification asyncTemplate(String uriTemplate) {
41+
return new McpServerFeatures.AsyncResourceTemplateSpecification(
42+
McpSchema.ResourceTemplate.builder(uriTemplate, uriTemplate).build(),
43+
(exchange, request) -> Mono.empty());
44+
}
45+
46+
private static McpServerFeatures.AsyncPromptSpecification asyncPrompt(String name) {
47+
return new McpServerFeatures.AsyncPromptSpecification(McpSchema.Prompt.builder(name).build(),
48+
(exchange, request) -> Mono.empty());
49+
}
50+
51+
@Test
52+
void addAndRemoveToolWithDerivedCapabilities() {
53+
MockMcpServerTransport transport = new MockMcpServerTransport();
54+
McpAsyncServer server = McpServer.async(new MockMcpServerTransportProvider(transport))
55+
.serverInfo(SERVER_INFO)
56+
.tools(asyncTool("initial"))
57+
.build();
58+
59+
assertThat(server.getServerCapabilities().tools().listChanged()).isNull();
60+
61+
StepVerifier.create(server.addTool(asyncTool("added"))).verifyComplete();
62+
StepVerifier.create(server.removeTool("initial")).verifyComplete();
63+
64+
assertThat(server.listTools().map(McpSchema.Tool::name).collectList().block()).containsExactly("added");
65+
assertThat(transport.getAllSentMessages()).isEmpty();
66+
}
67+
68+
@Test
69+
void addAndRemoveResourceAndTemplateWithDerivedCapabilities() {
70+
MockMcpServerTransport transport = new MockMcpServerTransport();
71+
McpAsyncServer server = McpServer.async(new MockMcpServerTransportProvider(transport))
72+
.serverInfo(SERVER_INFO)
73+
.resources(asyncResource("test://initial"))
74+
.build();
75+
76+
assertThat(server.getServerCapabilities().resources().listChanged()).isNull();
77+
78+
StepVerifier.create(server.addResource(asyncResource("test://added"))).verifyComplete();
79+
StepVerifier.create(server.removeResource("test://initial")).verifyComplete();
80+
StepVerifier.create(server.addResourceTemplate(asyncTemplate("test://items/{id}"))).verifyComplete();
81+
82+
assertThat(transport.getAllSentMessages()).isEmpty();
83+
}
84+
85+
@Test
86+
void addAndRemovePromptWithDerivedCapabilities() {
87+
MockMcpServerTransport transport = new MockMcpServerTransport();
88+
McpAsyncServer server = McpServer.async(new MockMcpServerTransportProvider(transport))
89+
.serverInfo(SERVER_INFO)
90+
.prompts(asyncPrompt("initial"))
91+
.build();
92+
93+
assertThat(server.getServerCapabilities().prompts().listChanged()).isNull();
94+
95+
StepVerifier.create(server.addPrompt(asyncPrompt("added"))).verifyComplete();
96+
StepVerifier.create(server.removePrompt("initial")).verifyComplete();
97+
98+
assertThat(transport.getAllSentMessages()).isEmpty();
99+
}
100+
101+
@Test
102+
void syncServerAddAndRemoveToolWithDerivedCapabilities() {
103+
McpSyncServer server = McpServer.sync(new MockMcpServerTransportProvider(new MockMcpServerTransport()))
104+
.serverInfo(SERVER_INFO)
105+
.tools(McpServerFeatures.SyncToolSpecification.builder()
106+
.tool(McpSchema.Tool.builder("initial").description("initial").build())
107+
.callHandler((exchange, request) -> McpSchema.CallToolResult.builder().build())
108+
.build())
109+
.build();
110+
111+
assertThatCode(() -> server.addTool(McpServerFeatures.SyncToolSpecification.builder()
112+
.tool(McpSchema.Tool.builder("added").description("added").build())
113+
.callHandler((exchange, request) -> McpSchema.CallToolResult.builder().build())
114+
.build())).doesNotThrowAnyException();
115+
assertThatCode(() -> server.removeTool("initial")).doesNotThrowAnyException();
116+
}
117+
118+
}

0 commit comments

Comments
 (0)