Skip to content

Commit 489b91b

Browse files
authored
feat: Add stateless MCP operation support and improve documentation (#16)
- Introduced stateless method callback and provider classes for Complete, Prompt, Resource, and Tool operations using . - Added and method callbacks and providers for all MCP operation types. - Updated README to document stateless support, new callback/provider classes, and usage examples. - Improved Spring integration to support stateless MCP operations. - Added comprehensive tests for stateless method callbacks and providers. - Fixed minor issues and improved parameter validation in method callbacks. - Updated dependency to MCP Java SDK 0.11.2. Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
1 parent cd7c67b commit 489b91b

File tree

46 files changed

+10754
-299
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+10754
-299
lines changed

README.md

Lines changed: 234 additions & 9 deletions
Large diffs are not rendered by default.

mcp-annotations-spring/src/main/java/org/springaicommunity/mcp/spring/AsyncMcpAnnotationProvider.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@
2323
import org.springaicommunity.mcp.provider.AsyncMcpLoggingConsumerProvider;
2424
import org.springaicommunity.mcp.provider.AsyncMcpSamplingProvider;
2525
import org.springaicommunity.mcp.provider.AsyncMcpToolProvider;
26+
import org.springaicommunity.mcp.provider.AsyncStatelessMcpPromptProvider;
27+
import org.springaicommunity.mcp.provider.AsyncStatelessMcpResourceProvider;
28+
import org.springaicommunity.mcp.provider.AsyncStatelessMcpToolProvider;
2629

2730
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
31+
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
2832
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
2933
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
3034
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
@@ -89,6 +93,45 @@ protected Method[] doGetClassMethods(Object bean) {
8993

9094
}
9195

96+
private static class SpringAiAsyncStatelessMcpToolProvider extends AsyncStatelessMcpToolProvider {
97+
98+
public SpringAiAsyncStatelessMcpToolProvider(List<Object> toolObjects) {
99+
super(toolObjects);
100+
}
101+
102+
@Override
103+
protected Method[] doGetClassMethods(Object bean) {
104+
return AnnotationProviderUtil.beanMethods(bean);
105+
}
106+
107+
}
108+
109+
private static class SpringAiAsyncStatelessPromptProvider extends AsyncStatelessMcpPromptProvider {
110+
111+
public SpringAiAsyncStatelessPromptProvider(List<Object> promptObjects) {
112+
super(promptObjects);
113+
}
114+
115+
@Override
116+
protected Method[] doGetClassMethods(Object bean) {
117+
return AnnotationProviderUtil.beanMethods(bean);
118+
}
119+
120+
}
121+
122+
private static class SpringAiAsyncStatelessResourceProvider extends AsyncStatelessMcpResourceProvider {
123+
124+
public SpringAiAsyncStatelessResourceProvider(List<Object> resourceObjects) {
125+
super(resourceObjects);
126+
}
127+
128+
@Override
129+
protected Method[] doGetClassMethods(Object bean) {
130+
return AnnotationProviderUtil.beanMethods(bean);
131+
}
132+
133+
}
134+
92135
public static List<Function<LoggingMessageNotification, Mono<Void>>> createAsyncLoggingConsumers(
93136
List<Object> loggingObjects) {
94137
return new SpringAiAsyncMcpLoggingConsumerProvider(loggingObjects).getLoggingConsumers();
@@ -108,4 +151,19 @@ public static List<AsyncToolSpecification> createAsyncToolSpecifications(List<Ob
108151
return new SpringAiAsyncMcpToolProvider(toolObjects).getToolSpecifications();
109152
}
110153

154+
public static List<McpStatelessServerFeatures.AsyncToolSpecification> createAsyncStatelessToolSpecifications(
155+
List<Object> toolObjects) {
156+
return new SpringAiAsyncStatelessMcpToolProvider(toolObjects).getToolSpecifications();
157+
}
158+
159+
public static List<McpStatelessServerFeatures.AsyncPromptSpecification> createAsyncStatelessPromptSpecifications(
160+
List<Object> promptObjects) {
161+
return new SpringAiAsyncStatelessPromptProvider(promptObjects).getPromptSpecifications();
162+
}
163+
164+
public static List<McpStatelessServerFeatures.AsyncResourceSpecification> createAsyncStatelessResourceSpecifications(
165+
List<Object> resourceObjects) {
166+
return new SpringAiAsyncStatelessResourceProvider(resourceObjects).getResourceSpecifications();
167+
}
168+
111169
}

mcp-annotations-spring/src/main/java/org/springaicommunity/mcp/spring/SyncMcpAnnotationProvider.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,15 @@
2727
import org.springaicommunity.mcp.provider.SyncMcpResourceProvider;
2828
import org.springaicommunity.mcp.provider.SyncMcpSamplingProvider;
2929
import org.springaicommunity.mcp.provider.SyncMcpToolProvider;
30+
import org.springaicommunity.mcp.provider.SyncStatelessMcpPromptProvider;
31+
import org.springaicommunity.mcp.provider.SyncStatelessMcpResourceProvider;
32+
import org.springaicommunity.mcp.provider.SyncStatelessMcpToolProvider;
3033

3134
import io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification;
3235
import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification;
3336
import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification;
3437
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
38+
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
3539
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
3640
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
3741
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
@@ -69,6 +73,19 @@ protected Method[] doGetClassMethods(Object bean) {
6973

7074
}
7175

76+
private static class SpringAiSyncStatelessToolProvider extends SyncStatelessMcpToolProvider {
77+
78+
public SpringAiSyncStatelessToolProvider(List<Object> toolObjects) {
79+
super(toolObjects);
80+
}
81+
82+
@Override
83+
protected Method[] doGetClassMethods(Object bean) {
84+
return AnnotationProviderUtil.beanMethods(bean);
85+
}
86+
87+
}
88+
7289
private static class SpringAiSyncMcpPromptProvider extends SyncMcpPromptProvider {
7390

7491
public SpringAiSyncMcpPromptProvider(List<Object> promptObjects) {
@@ -82,6 +99,19 @@ protected Method[] doGetClassMethods(Object bean) {
8299

83100
};
84101

102+
private static class SpringAiSyncStatelessPromptProvider extends SyncStatelessMcpPromptProvider {
103+
104+
public SpringAiSyncStatelessPromptProvider(List<Object> promptObjects) {
105+
super(promptObjects);
106+
}
107+
108+
@Override
109+
protected Method[] doGetClassMethods(Object bean) {
110+
return AnnotationProviderUtil.beanMethods(bean);
111+
}
112+
113+
}
114+
85115
private static class SpringAiSyncMcpResourceProvider extends SyncMcpResourceProvider {
86116

87117
public SpringAiSyncMcpResourceProvider(List<Object> resourceObjects) {
@@ -95,6 +125,19 @@ protected Method[] doGetClassMethods(Object bean) {
95125

96126
}
97127

128+
private static class SpringAiSyncStatelessResourceProvider extends SyncStatelessMcpResourceProvider {
129+
130+
public SpringAiSyncStatelessResourceProvider(List<Object> resourceObjects) {
131+
super(resourceObjects);
132+
}
133+
134+
@Override
135+
protected Method[] doGetClassMethods(Object bean) {
136+
return AnnotationProviderUtil.beanMethods(bean);
137+
}
138+
139+
}
140+
98141
private static class SpringAiSyncMcpLoggingConsumerProvider extends SyncMcpLoggingConsumerProvider {
99142

100143
public SpringAiSyncMcpLoggingConsumerProvider(List<Object> loggingObjects) {
@@ -138,6 +181,11 @@ public static List<SyncToolSpecification> createSyncToolSpecifications(List<Obje
138181
return new SpringAiSyncToolProvider(toolObjects).getToolSpecifications();
139182
}
140183

184+
public static List<McpStatelessServerFeatures.SyncToolSpecification> createSyncStatelessToolSpecifications(
185+
List<Object> toolObjects) {
186+
return new SpringAiSyncStatelessToolProvider(toolObjects).getToolSpecifications();
187+
}
188+
141189
public static List<SyncCompletionSpecification> createSyncCompleteSpecifications(List<Object> completeObjects) {
142190
return new SpringAiSyncMcpCompletionProvider(completeObjects).getCompleteSpecifications();
143191
}
@@ -146,10 +194,20 @@ public static List<SyncPromptSpecification> createSyncPromptSpecifications(List<
146194
return new SpringAiSyncMcpPromptProvider(promptObjects).getPromptSpecifications();
147195
}
148196

197+
public static List<McpStatelessServerFeatures.SyncPromptSpecification> createSyncStatelessPromptSpecifications(
198+
List<Object> promptObjects) {
199+
return new SpringAiSyncStatelessPromptProvider(promptObjects).getPromptSpecifications();
200+
}
201+
149202
public static List<SyncResourceSpecification> createSyncResourceSpecifications(List<Object> resourceObjects) {
150203
return new SpringAiSyncMcpResourceProvider(resourceObjects).getResourceSpecifications();
151204
}
152205

206+
public static List<McpStatelessServerFeatures.SyncResourceSpecification> createSyncStatelessResourceSpecifications(
207+
List<Object> resourceObjects) {
208+
return new SpringAiSyncStatelessResourceProvider(resourceObjects).getResourceSpecifications();
209+
}
210+
153211
public static List<Consumer<LoggingMessageNotification>> createSyncLoggingConsumers(List<Object> loggingObjects) {
154212
return new SpringAiSyncMcpLoggingConsumerProvider(loggingObjects).getLoggingConsumers();
155213
}

mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/PromptAdaptor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public static McpSchema.Prompt asPrompt(McpPrompt mcpPrompt, Method method) {
4444

4545
private static String getName(McpPrompt promptAnnotation, Method method) {
4646
Assert.notNull(method, "method cannot be null");
47-
if (promptAnnotation == null || (promptAnnotation.name() == null)) {
47+
if (promptAnnotation == null || (promptAnnotation.name() == null) || promptAnnotation.name().isEmpty()) {
4848
return method.getName();
4949
}
5050
return promptAnnotation.name();
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*/
4+
5+
package org.springaicommunity.mcp.method.complete;
6+
7+
import java.lang.reflect.Method;
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import java.util.function.BiFunction;
11+
12+
import org.springaicommunity.mcp.annotation.McpComplete;
13+
14+
import io.modelcontextprotocol.server.McpTransportContext;
15+
import io.modelcontextprotocol.spec.McpSchema.CompleteRequest;
16+
import io.modelcontextprotocol.spec.McpSchema.CompleteResult;
17+
import io.modelcontextprotocol.spec.McpSchema.CompleteResult.CompleteCompletion;
18+
import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
19+
import reactor.core.publisher.Mono;
20+
21+
/**
22+
* Class for creating BiFunction callbacks around complete methods with asynchronous
23+
* processing for stateless contexts.
24+
*
25+
* This class provides a way to convert methods annotated with {@link McpComplete} into
26+
* callback functions that can be used to handle completion requests asynchronously in
27+
* stateless environments. It supports various method signatures and return types, and
28+
* handles both prompt and URI template completions.
29+
*
30+
* @author Christian Tzolov
31+
*/
32+
public final class AsyncStatelessMcpCompleteMethodCallback extends AbstractMcpCompleteMethodCallback
33+
implements BiFunction<McpTransportContext, CompleteRequest, Mono<CompleteResult>> {
34+
35+
private AsyncStatelessMcpCompleteMethodCallback(Builder builder) {
36+
super(builder.method, builder.bean, builder.prompt, builder.uri, builder.uriTemplateManagerFactory);
37+
this.validateMethod(this.method);
38+
}
39+
40+
/**
41+
* Apply the callback to the given context and request.
42+
* <p>
43+
* This method builds the arguments for the method call, invokes the method, and
44+
* converts the result to a CompleteResult.
45+
* @param context The transport context, may be null if the method doesn't require it
46+
* @param request The complete request, must not be null
47+
* @return A Mono that emits the complete result
48+
* @throws McpCompleteMethodException if there is an error invoking the complete
49+
* method
50+
* @throws IllegalArgumentException if the request is null
51+
*/
52+
@Override
53+
public Mono<CompleteResult> apply(McpTransportContext context, CompleteRequest request) {
54+
if (request == null) {
55+
return Mono.error(new IllegalArgumentException("Request must not be null"));
56+
}
57+
58+
return Mono.defer(() -> {
59+
try {
60+
// Build arguments for the method call
61+
Object[] args = this.buildArgs(this.method, context, request);
62+
63+
// Invoke the method
64+
this.method.setAccessible(true);
65+
Object result = this.method.invoke(this.bean, args);
66+
67+
// Handle the result based on its type
68+
if (result instanceof Mono<?>) {
69+
// If the result is already a Mono, map it to a CompleteResult
70+
return ((Mono<?>) result).map(r -> convertToCompleteResult(r));
71+
}
72+
else {
73+
// Otherwise, convert the result to a CompleteResult and wrap in a
74+
// Mono
75+
return Mono.just(convertToCompleteResult(result));
76+
}
77+
}
78+
catch (Exception e) {
79+
return Mono.error(
80+
new McpCompleteMethodException("Error invoking complete method: " + this.method.getName(), e));
81+
}
82+
});
83+
}
84+
85+
/**
86+
* Converts a result object to a CompleteResult.
87+
* @param result The result object
88+
* @return The CompleteResult
89+
*/
90+
private CompleteResult convertToCompleteResult(Object result) {
91+
if (result == null) {
92+
return new CompleteResult(new CompleteCompletion(List.of(), 0, false));
93+
}
94+
95+
if (result instanceof CompleteResult) {
96+
return (CompleteResult) result;
97+
}
98+
99+
if (result instanceof CompleteCompletion) {
100+
return new CompleteResult((CompleteCompletion) result);
101+
}
102+
103+
if (result instanceof List) {
104+
List<?> list = (List<?>) result;
105+
List<String> values = new ArrayList<>();
106+
107+
for (Object item : list) {
108+
if (item instanceof String) {
109+
values.add((String) item);
110+
}
111+
else {
112+
throw new IllegalArgumentException("List items must be of type String");
113+
}
114+
}
115+
116+
return new CompleteResult(new CompleteCompletion(values, values.size(), false));
117+
}
118+
119+
if (result instanceof String) {
120+
return new CompleteResult(new CompleteCompletion(List.of((String) result), 1, false));
121+
}
122+
123+
throw new IllegalArgumentException("Unsupported return type: " + result.getClass().getName());
124+
}
125+
126+
/**
127+
* Builder for creating AsyncStatelessMcpCompleteMethodCallback instances.
128+
* <p>
129+
* This builder provides a fluent API for constructing
130+
* AsyncStatelessMcpCompleteMethodCallback instances with the required parameters.
131+
*/
132+
public static class Builder extends AbstractBuilder<Builder, AsyncStatelessMcpCompleteMethodCallback> {
133+
134+
/**
135+
* Constructor for Builder.
136+
*/
137+
public Builder() {
138+
this.uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
139+
}
140+
141+
/**
142+
* Build the callback.
143+
* @return A new AsyncStatelessMcpCompleteMethodCallback instance
144+
*/
145+
@Override
146+
public AsyncStatelessMcpCompleteMethodCallback build() {
147+
validate();
148+
return new AsyncStatelessMcpCompleteMethodCallback(this);
149+
}
150+
151+
}
152+
153+
/**
154+
* Create a new builder.
155+
* @return A new builder instance
156+
*/
157+
public static Builder builder() {
158+
return new Builder();
159+
}
160+
161+
/**
162+
* Validates that the method return type is compatible with the complete callback.
163+
* @param method The method to validate
164+
* @throws IllegalArgumentException if the return type is not compatible
165+
*/
166+
@Override
167+
protected void validateReturnType(Method method) {
168+
Class<?> returnType = method.getReturnType();
169+
170+
boolean validReturnType = CompleteResult.class.isAssignableFrom(returnType)
171+
|| CompleteCompletion.class.isAssignableFrom(returnType) || List.class.isAssignableFrom(returnType)
172+
|| String.class.isAssignableFrom(returnType) || Mono.class.isAssignableFrom(returnType);
173+
174+
if (!validReturnType) {
175+
throw new IllegalArgumentException(
176+
"Method must return either CompleteResult, CompleteCompletion, List<String>, "
177+
+ "String, or Mono<T>: " + method.getName() + " in " + method.getDeclaringClass().getName()
178+
+ " returns " + returnType.getName());
179+
}
180+
}
181+
182+
/**
183+
* Checks if a parameter type is compatible with the exchange type.
184+
* @param paramType The parameter type to check
185+
* @return true if the parameter type is compatible with the exchange type, false
186+
* otherwise
187+
*/
188+
@Override
189+
protected boolean isExchangeType(Class<?> paramType) {
190+
return McpTransportContext.class.isAssignableFrom(paramType);
191+
}
192+
193+
}

0 commit comments

Comments
 (0)