Skip to content

Commit b5d3011

Browse files
authored
refactor: improve bidirectional capability handling and error reporting (spring-ai-community#73)
Major changes: - Add capability check methods (rootsEnabled, elicitEnabled, sampleEnabled) to both sync and async request contexts - Replace Optional return types with direct returns and throw IllegalStateException when capabilities not supported - Change from warning logs to exceptions for unsupported operations in stateless contexts - Removed stateless context implementations (StatelessAsyncRequestContext, StatelessMcpSyncRequestContext) - Add bidirectional parameter filtering for stateless providers (tools, resources, prompts, completions) - Update tests to verify new exception-based error handling - Updated README Breaking changes: - McpSyncRequestContext methods now return direct types instead of Optional<T> - McpAsyncRequestContext methods now return Mono.error() instead of Mono.empty() for unsupported operations - Stateless context methods throw IllegalStateException instead of logging warnings This improves API clarity by making capability checks explicit and error handling more predictable. Signed-off-by: Christian Tzolov <[email protected]>
1 parent 16cfb8a commit b5d3011

22 files changed

+879
-572
lines changed

README.md

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -920,18 +920,21 @@ public String processWithContext(
920920
// Use exchange for additional operations...
921921
}
922922

923-
// Perform elicitation with default message - returns StructuredElicitResult
924-
Optional<StructuredElicitResult<UserInfo>> result = context.elicit(new TypeReference<UserInfo>() {});
925-
926-
// Or perform elicitation with custom configuration - returns StructuredElicitResult
927-
Optional<StructuredElicitResult<UserInfo>> structuredResult = context.elicit(
928-
e -> e.message("Please provide your information").meta("context", "user-registration"),
929-
new TypeReference<UserInfo>() {}
930-
);
931-
932-
if (structuredResult.isPresent() && structuredResult.get().action() == ElicitResult.Action.ACCEPT) {
933-
UserInfo info = structuredResult.get().structuredContent();
934-
return "Processed: " + data + " for user " + info.name();
923+
// Check if elicitation is supported before using it
924+
if (context.elicitEnabled()) {
925+
// Perform elicitation with default message - returns StructuredElicitResult
926+
StructuredElicitResult<UserInfo> result = context.elicit(new TypeReference<UserInfo>() {});
927+
928+
// Or perform elicitation with custom configuration - returns StructuredElicitResult
929+
StructuredElicitResult<UserInfo> structuredResult = context.elicit(
930+
e -> e.message("Please provide your information").meta("context", "user-registration"),
931+
new TypeReference<UserInfo>() {}
932+
);
933+
934+
if (structuredResult.action() == ElicitResult.Action.ACCEPT) {
935+
UserInfo info = structuredResult.structuredContent();
936+
return "Processed: " + data + " for user " + info.name();
937+
}
935938
}
936939

937940
return "Processed: " + data;
@@ -962,10 +965,14 @@ public GetPromptResult generateWithContext(
962965
// Log prompt generation
963966
context.info("Generating prompt for topic: " + topic);
964967

965-
// Perform sampling if needed
966-
Optional<CreateMessageResult> samplingResult = context.sample(
967-
"What are the key points about " + topic + "?"
968-
);
968+
// Check if sampling is supported before using it
969+
if (context.sampleEnabled()) {
970+
// Perform sampling if needed
971+
CreateMessageResult samplingResult = context.sample(
972+
"What are the key points about " + topic + "?"
973+
);
974+
// Use sampling result...
975+
}
969976

970977
String message = "Let's discuss " + topic;
971978
return new GetPromptResult("Generated Prompt",
@@ -1059,16 +1066,24 @@ public Mono<GetPromptResult> asyncGenerateWithContext(
10591066
- `log(Consumer<LoggingSpec>)` - Send log messages with custom configuration
10601067
- `debug(String)`, `info(String)`, `warn(String)`, `error(String)` - Convenience logging methods
10611068
- `progress(int)`, `progress(Consumer<ProgressSpec>)` - Send progress updates
1062-
- `elicit(TypeReference<T>)` - Request user input with default message, returns `StructuredElicitResult<T>` with action, typed content, and metadata
1063-
- `elicit(Class<T>)` - Request user input with default message using Class type, returns `StructuredElicitResult<T>`
1064-
- `elicit(Consumer<ElicitationSpec>, TypeReference<T>)` - Request user input with custom configuration, returns `StructuredElicitResult<T>`
1065-
- `elicit(Consumer<ElicitationSpec>, Class<T>)` - Request user input with custom configuration using Class type, returns `StructuredElicitResult<T>`
1066-
- `elicit(ElicitRequest)` - Request user input with full control over the elicitation request
1067-
- `sample(...)` - Request LLM sampling with various configuration options
1068-
- `roots()` - Access root directories (returns `Optional<ListRootsResult>`)
1069+
- `rootsEnabled()` - Check if roots capability is supported by the client
1070+
- `roots()` - Access root directories (throws `IllegalStateException` if not supported)
1071+
- `elicitEnabled()` - Check if elicitation capability is supported by the client
1072+
- `elicit(TypeReference<T>)` - Request user input with default message, returns `StructuredElicitResult<T>` with action, typed content, and metadata (throws `IllegalStateException` if not supported)
1073+
- `elicit(Class<T>)` - Request user input with default message using Class type, returns `StructuredElicitResult<T>` (throws `IllegalStateException` if not supported)
1074+
- `elicit(Consumer<ElicitationSpec>, TypeReference<T>)` - Request user input with custom configuration, returns `StructuredElicitResult<T>` (throws `IllegalStateException` if not supported)
1075+
- `elicit(Consumer<ElicitationSpec>, Class<T>)` - Request user input with custom configuration using Class type, returns `StructuredElicitResult<T>` (throws `IllegalStateException` if not supported)
1076+
- `elicit(ElicitRequest)` - Request user input with full control over the elicitation request (throws `IllegalStateException` if not supported)
1077+
- `sampleEnabled()` - Check if sampling capability is supported by the client
1078+
- `sample(...)` - Request LLM sampling with various configuration options (throws `IllegalStateException` if not supported)
10691079
- `ping()` - Send ping to check connection
10701080

1071-
`McpAsyncRequestContext` provides the same methods but with reactive return types (`Mono<T>` instead of `T` or `Optional<T>`).
1081+
`McpAsyncRequestContext` provides the same methods but with reactive return types (`Mono<T>` instead of `T`). Methods that throw `IllegalStateException` in sync context return `Mono.error(IllegalStateException)` in async context.
1082+
1083+
**Important Notes on Capability Checking:**
1084+
- Always check capability support using `rootsEnabled()`, `elicitEnabled()`, or `sampleEnabled()` before calling the corresponding methods
1085+
- Calling capability methods when not supported will throw `IllegalStateException` (sync) or return `Mono.error()` (async)
1086+
- Stateless servers do not support bidirectional operations (roots, elicitation, sampling) and will always return `false` for capability checks
10721087

10731088
This unified context approach simplifies method signatures and provides a consistent API across different operation types and execution modes (stateful vs stateless, sync vs async).
10741089

@@ -2104,6 +2119,9 @@ public class StatelessResourceProvider {
21042119
}
21052120
```
21062121

2122+
**Important Note on Stateless Operations:**
2123+
Stateless server methods cannot use bidirectional parameters like `McpSyncRequestContext`, `McpAsyncRequestContext`, `McpSyncServerExchange`, or `McpAsyncServerExchange`. These parameters require client capabilities (roots, elicitation, sampling) that are not available in stateless mode. Methods with these parameters will be automatically filtered out and not registered as stateless operations.
2124+
21072125
#### Stateless Tool Example
21082126

21092127
```java

mcp-annotations/src/main/java/org/springaicommunity/mcp/context/DefaultMcpAsyncRequestContext.java

Lines changed: 39 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,31 @@ private DefaultMcpAsyncRequestContext(McpSchema.Request request, McpAsyncServerE
5959

6060
// Roots
6161

62+
@Override
63+
public Mono<Boolean> rootsEnabled() {
64+
return Mono.just(!(this.exchange.getClientCapabilities() == null
65+
|| this.exchange.getClientCapabilities().roots() == null));
66+
}
67+
6268
@Override
6369
public Mono<ListRootsResult> roots() {
64-
if (this.exchange.getClientCapabilities() == null || this.exchange.getClientCapabilities().roots() == null) {
65-
logger.warn("Roots not supported by the client! Ignoring the roots request for request:" + this.request);
66-
return Mono.empty();
67-
}
68-
return this.exchange.listRoots();
70+
return this.rootsEnabled().flatMap(enabled -> {
71+
if (!enabled) {
72+
return Mono.error(new IllegalStateException(
73+
"Roots not supported by the client: " + this.exchange.getClientInfo()));
74+
}
75+
return this.exchange.listRoots();
76+
});
6977
}
7078

7179
// Elicitation
7280

81+
@Override
82+
public Mono<Boolean> elicitEnabled() {
83+
return Mono.just(!(this.exchange.getClientCapabilities() == null
84+
|| this.exchange.getClientCapabilities().elicitation() == null));
85+
}
86+
7387
@Override
7488
public <T> Mono<StructuredElicitResult<T>> elicit(Consumer<ElicitationSpec> spec, TypeReference<T> type) {
7589
Assert.notNull(type, "Elicitation response type must not be null");
@@ -112,14 +126,13 @@ public <T> Mono<StructuredElicitResult<T>> elicit(Class<T> type) {
112126
public Mono<ElicitResult> elicit(ElicitRequest elicitRequest) {
113127
Assert.notNull(elicitRequest, "Elicit request must not be null");
114128

115-
if (this.exchange.getClientCapabilities() == null
116-
|| this.exchange.getClientCapabilities().elicitation() == null) {
117-
logger.warn("Elicitation not supported by the client! Ignoring the elicitation request for request:"
118-
+ elicitRequest);
119-
return Mono.empty();
120-
}
121-
122-
return this.exchange.createElicitation(elicitRequest);
129+
return this.elicitEnabled().flatMap(enabled -> {
130+
if (!enabled) {
131+
return Mono.error(new IllegalStateException(
132+
"Elicitation not supported by the client: " + this.exchange.getClientInfo()));
133+
}
134+
return this.exchange.createElicitation(elicitRequest);
135+
});
123136
}
124137

125138
public Mono<ElicitResult> elicitationInternal(String message, Type type, Map<String, Object> meta) {
@@ -143,6 +156,12 @@ private Map<String, Object> generateElicitSchema(Type type) {
143156

144157
// Sampling
145158

159+
@Override
160+
public Mono<Boolean> sampleEnabled() {
161+
return Mono.just(!(this.exchange.getClientCapabilities() == null
162+
|| this.exchange.getClientCapabilities().sampling() == null));
163+
}
164+
146165
@Override
147166
public Mono<CreateMessageResult> sample(String... messages) {
148167
return this.sample(s -> s.message(messages));
@@ -176,14 +195,13 @@ public Mono<CreateMessageResult> sample(Consumer<SamplingSpec> samplingSpec) {
176195
@Override
177196
public Mono<CreateMessageResult> sample(CreateMessageRequest createMessageRequest) {
178197

179-
// check if supported
180-
if (this.exchange.getClientCapabilities() == null || this.exchange.getClientCapabilities().sampling() == null) {
181-
logger.warn("Sampling not supported by the client! Ignoring the sampling request for messages:"
182-
+ createMessageRequest);
183-
return Mono.empty();
184-
}
185-
186-
return this.exchange.createMessage(createMessageRequest);
198+
return this.sampleEnabled().flatMap(enabled -> {
199+
if (!enabled) {
200+
return Mono.error(new IllegalStateException(
201+
"Sampling not supported by the client: " + this.exchange.getClientInfo()));
202+
}
203+
return this.exchange.createMessage(createMessageRequest);
204+
});
187205
}
188206

189207
// Progress
@@ -317,10 +335,6 @@ public static class Builder {
317335

318336
private McpAsyncServerExchange exchange;
319337

320-
private boolean isStateless = false;
321-
322-
private McpTransportContext transportContext;
323-
324338
private Builder() {
325339
}
326340

@@ -334,178 +348,10 @@ public Builder exchange(McpAsyncServerExchange exchange) {
334348
return this;
335349
}
336350

337-
public Builder stateless(boolean isStateless) {
338-
this.isStateless = isStateless;
339-
return this;
340-
}
341-
342-
public Builder transportContext(McpTransportContext transportContext) {
343-
this.transportContext = transportContext;
344-
return this;
345-
}
346-
347351
public McpAsyncRequestContext build() {
348-
if (this.isStateless) {
349-
return new StatelessAsyncRequestContext(this.request, this.transportContext);
350-
}
351352
return new DefaultMcpAsyncRequestContext(this.request, this.exchange);
352353
}
353354

354355
}
355356

356-
private static class StatelessAsyncRequestContext implements McpAsyncRequestContext {
357-
358-
private final McpSchema.Request request;
359-
360-
private McpTransportContext transportContext;
361-
362-
public StatelessAsyncRequestContext(McpSchema.Request request, McpTransportContext transportContext) {
363-
this.request = request;
364-
this.transportContext = transportContext;
365-
}
366-
367-
@Override
368-
public Mono<ListRootsResult> roots() {
369-
logger.warn("Roots not supported by the client! Ignoring the roots request");
370-
return Mono.empty();
371-
}
372-
373-
@Override
374-
public <T> Mono<StructuredElicitResult<T>> elicit(Consumer<ElicitationSpec> spec, TypeReference<T> returnType) {
375-
logger.warn("Elicitation not supported by the client! Ignoring the elicitation request");
376-
return Mono.empty();
377-
}
378-
379-
@Override
380-
public <T> Mono<StructuredElicitResult<T>> elicit(TypeReference<T> type) {
381-
logger.warn("Elicitation not supported by the client! Ignoring the elicitation request");
382-
return Mono.empty();
383-
}
384-
385-
@Override
386-
public <T> Mono<StructuredElicitResult<T>> elicit(Consumer<ElicitationSpec> spec, Class<T> returnType) {
387-
logger.warn("Elicitation not supported by the client! Ignoring the elicitation request");
388-
return Mono.empty();
389-
}
390-
391-
@Override
392-
public <T> Mono<StructuredElicitResult<T>> elicit(Class<T> type) {
393-
logger.warn("Elicitation not supported by the client! Ignoring the elicitation request");
394-
return Mono.empty();
395-
}
396-
397-
@Override
398-
public Mono<ElicitResult> elicit(ElicitRequest elicitRequest) {
399-
logger.warn("Elicitation not supported by the client! Ignoring the elicitation request");
400-
return Mono.empty();
401-
}
402-
403-
@Override
404-
public Mono<CreateMessageResult> sample(String... messages) {
405-
logger.warn("Sampling not supported by the client! Ignoring the sampling request");
406-
return Mono.empty();
407-
}
408-
409-
@Override
410-
public Mono<CreateMessageResult> sample(Consumer<SamplingSpec> samplingSpec) {
411-
logger.warn("Sampling not supported by the client! Ignoring the sampling request");
412-
return Mono.empty();
413-
}
414-
415-
@Override
416-
public Mono<CreateMessageResult> sample(CreateMessageRequest createMessageRequest) {
417-
logger.warn("Sampling not supported by the client! Ignoring the sampling request");
418-
return Mono.empty();
419-
}
420-
421-
@Override
422-
public Mono<Void> progress(int progress) {
423-
logger.warn("Progress not supported by the client! Ignoring the progress request");
424-
return Mono.empty();
425-
}
426-
427-
@Override
428-
public Mono<Void> progress(Consumer<ProgressSpec> progressSpec) {
429-
logger.warn("Progress not supported by the client! Ignoring the progress request");
430-
return Mono.empty();
431-
}
432-
433-
@Override
434-
public Mono<Void> progress(ProgressNotification progressNotification) {
435-
logger.warn("Progress not supported by the client! Ignoring the progress request");
436-
return Mono.empty();
437-
}
438-
439-
@Override
440-
public Mono<Object> ping() {
441-
logger.warn("Ping not supported by the client! Ignoring the ping request");
442-
return Mono.empty();
443-
}
444-
445-
@Override
446-
public Mono<Void> log(Consumer<LoggingSpec> logSpec) {
447-
logger.warn("Logging not supported by the client! Ignoring the logging request");
448-
return Mono.empty();
449-
}
450-
451-
@Override
452-
public Mono<Void> debug(String message) {
453-
logger.warn("Debug not supported by the client! Ignoring the debug request");
454-
return Mono.empty();
455-
}
456-
457-
@Override
458-
public Mono<Void> info(String message) {
459-
logger.warn("Info not supported by the client! Ignoring the info request");
460-
return Mono.empty();
461-
}
462-
463-
@Override
464-
public Mono<Void> warn(String message) {
465-
logger.warn("Warn not supported by the client! Ignoring the warn request");
466-
return Mono.empty();
467-
}
468-
469-
@Override
470-
public Mono<Void> error(String message) {
471-
logger.warn("Error not supported by the client! Ignoring the error request");
472-
return Mono.empty();
473-
}
474-
475-
// Getters
476-
477-
public McpSchema.Request request() {
478-
return this.request;
479-
}
480-
481-
public McpAsyncServerExchange exchange() {
482-
logger.warn("Stateless servers do not support exchange! Returning null");
483-
return null;
484-
}
485-
486-
public String sessionId() {
487-
logger.warn("Stateless servers do not support session ID! Returning null");
488-
return null;
489-
}
490-
491-
public Implementation clientInfo() {
492-
logger.warn("Stateless servers do not support client info! Returning null");
493-
return null;
494-
}
495-
496-
public ClientCapabilities clientCapabilities() {
497-
logger.warn("Stateless servers do not support client capabilities! Returning null");
498-
return null;
499-
}
500-
501-
public Map<String, Object> requestMeta() {
502-
return this.request.meta();
503-
}
504-
505-
public McpTransportContext transportContext() {
506-
return transportContext;
507-
}
508-
509-
}
510-
511357
}

0 commit comments

Comments
 (0)