Skip to content

Commit 0bdeea7

Browse files
committed
Throw MCP Error on missing sampling and elicitation handlers in a client
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
1 parent 2b7ad23 commit 0bdeea7

File tree

4 files changed

+100
-29
lines changed

4 files changed

+100
-29
lines changed

mcp/mcp-annotations-spring/src/main/java/org/springframework/ai/mcp/annotation/spring/ClientMcpAsyncHandlersRegistry.java

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Map;
2323
import java.util.function.Function;
2424

25+
import io.modelcontextprotocol.spec.McpError;
2526
import io.modelcontextprotocol.spec.McpSchema;
2627
import org.slf4j.Logger;
2728
import org.slf4j.LoggerFactory;
@@ -101,8 +102,8 @@ public Mono<McpSchema.CreateMessageResult> handleSampling(String name,
101102
if (handler != null) {
102103
return handler.apply(samplingRequest);
103104
}
104-
// TODO: handle null
105-
return Mono.empty();
105+
return Mono.error(new McpError(new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND,
106+
"Sampling not supported", Map.of("reason", "Client does not have sampling capability"))));
106107
}
107108

108109
/**
@@ -116,8 +117,8 @@ public Mono<McpSchema.ElicitResult> handleElicitation(String name, McpSchema.Eli
116117
if (handler != null) {
117118
return handler.apply(elicitationRequest);
118119
}
119-
// TODO: handle null
120-
return Mono.empty();
120+
return Mono.error(new McpError(new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND,
121+
"Elicitation not supported", Map.of("reason", "Client does not have elicitation capability"))));
121122
}
122123

123124
/**
@@ -129,7 +130,6 @@ public Mono<Void> handleLogging(String name, McpSchema.LoggingMessageNotificatio
129130
logger.debug("Handling logging notification for client {}", name);
130131
var consumers = this.loggingHandlers.get(name);
131132
if (consumers == null) {
132-
// TODO handle
133133
return Mono.empty();
134134
}
135135
return Flux.fromIterable(consumers).flatMap(c -> c.apply(loggingMessageNotification)).then();
@@ -144,7 +144,6 @@ public Mono<Void> handleProgress(String name, McpSchema.ProgressNotification pro
144144
logger.debug("Handling progress notification for client {}", name);
145145
var consumers = this.progressHandlers.get(name);
146146
if (consumers == null) {
147-
// TODO handle
148147
return Mono.empty();
149148
}
150149
return Flux.fromIterable(consumers).flatMap(c -> c.apply(progressNotification)).then();
@@ -159,7 +158,6 @@ public Mono<Void> handleToolListChanged(String name, List<McpSchema.Tool> update
159158
logger.debug("Handling tool list changed notification for client {}", name);
160159
var consumers = this.toolListChangedHandlers.get(name);
161160
if (consumers == null) {
162-
// TODO handle
163161
return Mono.empty();
164162
}
165163
return Flux.fromIterable(consumers).flatMap(c -> c.apply(updatedTools)).then();
@@ -174,7 +172,6 @@ public Mono<Void> handlePromptListChanged(String name, List<McpSchema.Prompt> up
174172
logger.debug("Handling prompt list changed notification for client {}", name);
175173
var consumers = this.promptListChangedHandlers.get(name);
176174
if (consumers == null) {
177-
// TODO handle
178175
return Mono.empty();
179176
}
180177
return Flux.fromIterable(consumers).flatMap(c -> c.apply(updatedPrompts)).then();
@@ -189,7 +186,6 @@ public Mono<Void> handleResourceListChanged(String name, List<McpSchema.Resource
189186
logger.debug("Handling resource list changed notification for client {}", name);
190187
var consumers = this.resourceListChangedHandlers.get(name);
191188
if (consumers == null) {
192-
// TODO handle
193189
return Mono.empty();
194190
}
195191
return Flux.fromIterable(consumers).flatMap(c -> c.apply(updatedResources)).then();

mcp/mcp-annotations-spring/src/main/java/org/springframework/ai/mcp/annotation/spring/ClientMcpSyncHandlersRegistry.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.function.Consumer;
2424
import java.util.function.Function;
2525

26+
import io.modelcontextprotocol.spec.McpError;
2627
import io.modelcontextprotocol.spec.McpSchema;
2728
import org.slf4j.Logger;
2829
import org.slf4j.LoggerFactory;
@@ -100,8 +101,8 @@ public McpSchema.CreateMessageResult handleSampling(String name, McpSchema.Creat
100101
if (handler != null) {
101102
return handler.apply(samplingRequest);
102103
}
103-
// TODO: handle null
104-
return null;
104+
throw new McpError(new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND,
105+
"Sampling not supported", Map.of("reason", "Client does not have sampling capability")));
105106
}
106107

107108
/**
@@ -116,8 +117,8 @@ public McpSchema.ElicitResult handleElicitation(String name, McpSchema.ElicitReq
116117
if (handler != null) {
117118
return handler.apply(elicitationRequest);
118119
}
119-
// TODO: handle null
120-
return null;
120+
throw new McpError(new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND,
121+
"Elicitation not supported", Map.of("reason", "Client does not have elicitation capability")));
121122
}
122123

123124
/**

mcp/mcp-annotations-spring/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpAsyncHandlersRegistryTests.java

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
import java.util.List;
2222
import java.util.Map;
2323

24+
import io.modelcontextprotocol.spec.McpError;
2425
import io.modelcontextprotocol.spec.McpSchema;
25-
import org.junit.jupiter.api.Disabled;
2626
import org.junit.jupiter.api.Test;
2727
import org.springaicommunity.mcp.annotation.McpElicitation;
2828
import org.springaicommunity.mcp.annotation.McpLogging;
@@ -39,7 +39,7 @@
3939

4040
import static org.assertj.core.api.Assertions.assertThat;
4141
import static org.assertj.core.api.Assertions.assertThatThrownBy;
42-
import static org.assertj.core.api.Assertions.fail;
42+
import static org.assertj.core.api.InstanceOfAssertFactories.type;
4343

4444
class ClientMcpAsyncHandlersRegistryTests {
4545

@@ -147,6 +147,27 @@ void elicitation() {
147147
assertThat(response.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
148148
}
149149

150+
@Test
151+
void missingElicitationHandler() {
152+
var registry = new ClientMcpAsyncHandlersRegistry();
153+
var beanFactory = new DefaultListableBeanFactory();
154+
beanFactory.registerBeanDefinition("myConfig",
155+
BeanDefinitionBuilder
156+
.genericBeanDefinition(ClientMcpAsyncHandlersRegistryTests.HandlersConfiguration.class)
157+
.getBeanDefinition());
158+
registry.postProcessBeanFactory(beanFactory);
159+
registry.afterSingletonsInstantiated();
160+
161+
var request = McpSchema.ElicitRequest.builder().message("Elicit request").progressToken("token-12345").build();
162+
assertThatThrownBy(() -> registry.handleElicitation("client-unknown", request).block())
163+
.hasMessage("Elicitation not supported")
164+
.asInstanceOf(type(McpError.class))
165+
.extracting(McpError::getJsonRpcError)
166+
.satisfies(error -> assertThat(error.data())
167+
.isEqualTo(Map.of("reason", "Client does not have elicitation capability")))
168+
.satisfies(error -> assertThat(error.code()).isEqualTo(McpSchema.ErrorCodes.METHOD_NOT_FOUND));
169+
}
170+
150171
@Test
151172
void sampling() {
152173
var registry = new ClientMcpAsyncHandlersRegistry();
@@ -168,6 +189,30 @@ void sampling() {
168189
assertThat(content.text()).isEqualTo("Tell a joke");
169190
}
170191

192+
@Test
193+
void missingSamplingHandler() {
194+
var registry = new ClientMcpAsyncHandlersRegistry();
195+
var beanFactory = new DefaultListableBeanFactory();
196+
beanFactory.registerBeanDefinition("myConfig",
197+
BeanDefinitionBuilder
198+
.genericBeanDefinition(ClientMcpAsyncHandlersRegistryTests.HandlersConfiguration.class)
199+
.getBeanDefinition());
200+
registry.postProcessBeanFactory(beanFactory);
201+
registry.afterSingletonsInstantiated();
202+
203+
var request = McpSchema.CreateMessageRequest.builder()
204+
.messages(List
205+
.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Tell a joke"))))
206+
.build();
207+
assertThatThrownBy(() -> registry.handleSampling("client-unknown", request).block())
208+
.hasMessage("Sampling not supported")
209+
.asInstanceOf(type(McpError.class))
210+
.extracting(McpError::getJsonRpcError)
211+
.satisfies(error -> assertThat(error.data())
212+
.isEqualTo(Map.of("reason", "Client does not have sampling capability")))
213+
.satisfies(error -> assertThat(error.code()).isEqualTo(McpSchema.ErrorCodes.METHOD_NOT_FOUND));
214+
}
215+
171216
@Test
172217
void logging() {
173218
var registry = new ClientMcpAsyncHandlersRegistry();
@@ -295,12 +340,6 @@ void supportsProxiedClass() {
295340
assertThat(registry.getCapabilities("client-1").elicitation()).isNotNull();
296341
}
297342

298-
@Test
299-
@Disabled
300-
void missingHandler() {
301-
fail("TODO");
302-
}
303-
304343
static class ClientCapabilitiesConfiguration {
305344

306345
@McpElicitation(clients = { "client-1", "client-2" })

mcp/mcp-annotations-spring/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpSyncHandlersRegistryTests.java

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
import java.util.List;
2222
import java.util.Map;
2323

24+
import io.modelcontextprotocol.spec.McpError;
2425
import io.modelcontextprotocol.spec.McpSchema;
25-
import org.junit.jupiter.api.Disabled;
2626
import org.junit.jupiter.api.Test;
2727
import org.springaicommunity.mcp.annotation.McpElicitation;
2828
import org.springaicommunity.mcp.annotation.McpLogging;
@@ -38,7 +38,7 @@
3838

3939
import static org.assertj.core.api.Assertions.assertThat;
4040
import static org.assertj.core.api.Assertions.assertThatThrownBy;
41-
import static org.assertj.core.api.Assertions.fail;
41+
import static org.assertj.core.api.InstanceOfAssertFactories.type;
4242

4343
class ClientMcpSyncHandlersRegistryTests {
4444

@@ -145,6 +145,25 @@ void elicitation() {
145145
assertThat(response.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
146146
}
147147

148+
@Test
149+
void missingElicitationHandler() {
150+
var registry = new ClientMcpSyncHandlersRegistry();
151+
var beanFactory = new DefaultListableBeanFactory();
152+
beanFactory.registerBeanDefinition("myConfig",
153+
BeanDefinitionBuilder.genericBeanDefinition(HandlersConfiguration.class).getBeanDefinition());
154+
registry.postProcessBeanFactory(beanFactory);
155+
registry.afterSingletonsInstantiated();
156+
157+
var request = McpSchema.ElicitRequest.builder().message("Elicit request").progressToken("token-12345").build();
158+
assertThatThrownBy(() -> registry.handleElicitation("client-unknown", request))
159+
.hasMessage("Elicitation not supported")
160+
.asInstanceOf(type(McpError.class))
161+
.extracting(McpError::getJsonRpcError)
162+
.satisfies(error -> assertThat(error.data())
163+
.isEqualTo(Map.of("reason", "Client does not have elicitation capability")))
164+
.satisfies(error -> assertThat(error.code()).isEqualTo(McpSchema.ErrorCodes.METHOD_NOT_FOUND));
165+
}
166+
148167
@Test
149168
void sampling() {
150169
var registry = new ClientMcpSyncHandlersRegistry();
@@ -166,6 +185,28 @@ void sampling() {
166185
assertThat(content.text()).isEqualTo("Tell a joke");
167186
}
168187

188+
@Test
189+
void missingSamplingHandler() {
190+
var registry = new ClientMcpSyncHandlersRegistry();
191+
var beanFactory = new DefaultListableBeanFactory();
192+
beanFactory.registerBeanDefinition("myConfig",
193+
BeanDefinitionBuilder.genericBeanDefinition(HandlersConfiguration.class).getBeanDefinition());
194+
registry.postProcessBeanFactory(beanFactory);
195+
registry.afterSingletonsInstantiated();
196+
197+
var request = McpSchema.CreateMessageRequest.builder()
198+
.messages(List
199+
.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Tell a joke"))))
200+
.build();
201+
assertThatThrownBy(() -> registry.handleSampling("client-unknown", request))
202+
.hasMessage("Sampling not supported")
203+
.asInstanceOf(type(McpError.class))
204+
.extracting(McpError::getJsonRpcError)
205+
.satisfies(error -> assertThat(error.data())
206+
.isEqualTo(Map.of("reason", "Client does not have sampling capability")))
207+
.satisfies(error -> assertThat(error.code()).isEqualTo(McpSchema.ErrorCodes.METHOD_NOT_FOUND));
208+
}
209+
169210
@Test
170211
void logging() {
171212
var registry = new ClientMcpSyncHandlersRegistry();
@@ -291,12 +332,6 @@ void supportsProxiedClass() {
291332
assertThat(registry.getCapabilities("client-1").elicitation()).isNotNull();
292333
}
293334

294-
@Test
295-
@Disabled
296-
void missingHandler() {
297-
fail("TODO");
298-
}
299-
300335
static class ClientCapabilitiesConfiguration {
301336

302337
@McpElicitation(clients = { "client-1", "client-2" })

0 commit comments

Comments
 (0)