Skip to content

Commit 235765d

Browse files
authored
refactor: consolidate MCP tool callback hierarchy and improve error handling (#57)
- Introduce AbstractMcpToolMethodCallback as common base class to eliminate code duplication - Rename error handling methods for clarity: - createErrorResult -> createAsyncErrorResult/createSyncErrorResult - validateRequest -> validateSyncRequest - Improve error messages using findCauseUsingPlainJava(e) to extract and display root cause instead of wrapper exceptions Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
1 parent 5496f7b commit 235765d

12 files changed

+262
-329
lines changed

mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractAsyncMcpToolMethodCallback.java

Lines changed: 10 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,13 @@
1616

1717
package org.springaicommunity.mcp.method.tool;
1818

19-
import java.lang.reflect.InvocationTargetException;
2019
import java.lang.reflect.Method;
21-
import java.lang.reflect.Type;
22-
import java.util.Map;
23-
import java.util.stream.Stream;
2420

21+
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
22+
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
2523
import org.reactivestreams.Publisher;
26-
import org.springaicommunity.mcp.annotation.McpMeta;
27-
import org.springaicommunity.mcp.annotation.McpProgressToken;
2824
import org.springaicommunity.mcp.annotation.McpTool;
2925
import org.springaicommunity.mcp.method.tool.utils.JsonParser;
30-
31-
import com.fasterxml.jackson.core.type.TypeReference;
32-
33-
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
34-
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
3526
import reactor.core.publisher.Flux;
3627
import reactor.core.publisher.Mono;
3728

@@ -46,109 +37,16 @@
4637
* McpTransportContext)
4738
* @author Christian Tzolov
4839
*/
49-
public abstract class AbstractAsyncMcpToolMethodCallback<T> {
40+
public abstract class AbstractAsyncMcpToolMethodCallback<T> extends AbstractMcpToolMethodCallback<T> {
5041

5142
protected final Class<? extends Throwable> toolCallExceptionClass;
5243

53-
private static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeReference<Map<String, Object>>() {
54-
// No implementation needed
55-
};
56-
57-
protected final Method toolMethod;
58-
59-
protected final Object toolObject;
60-
61-
protected final ReturnMode returnMode;
62-
6344
protected AbstractAsyncMcpToolMethodCallback(ReturnMode returnMode, Method toolMethod, Object toolObject,
6445
Class<? extends Throwable> toolCallExceptionClass) {
65-
this.toolMethod = toolMethod;
66-
this.toolObject = toolObject;
67-
this.returnMode = returnMode;
46+
super(returnMode, toolMethod, toolObject);
6847
this.toolCallExceptionClass = toolCallExceptionClass;
6948
}
7049

71-
/**
72-
* Invokes the tool method with the provided arguments.
73-
* @param methodArguments The arguments to pass to the method
74-
* @return The result of the method invocation
75-
* @throws IllegalStateException if the method cannot be accessed
76-
* @throws RuntimeException if there's an error invoking the method
77-
*/
78-
protected Object callMethod(Object[] methodArguments) {
79-
this.toolMethod.setAccessible(true);
80-
81-
Object result;
82-
try {
83-
result = this.toolMethod.invoke(this.toolObject, methodArguments);
84-
}
85-
catch (IllegalAccessException ex) {
86-
throw new IllegalStateException("Could not access method: " + ex.getMessage(), ex);
87-
}
88-
catch (InvocationTargetException ex) {
89-
throw new RuntimeException("Error invoking method: " + this.toolMethod.getName(), ex);
90-
}
91-
return result;
92-
}
93-
94-
/**
95-
* Builds the method arguments from the context, tool input arguments, and optionally
96-
* the full request.
97-
* @param exchangeOrContext The exchange or context object (e.g.,
98-
* McpAsyncServerExchange or McpTransportContext)
99-
* @param toolInputArguments The input arguments from the tool request
100-
* @param request The full CallToolRequest (optional, can be null)
101-
* @return An array of method arguments
102-
*/
103-
protected Object[] buildMethodArguments(T exchangeOrContext, Map<String, Object> toolInputArguments,
104-
CallToolRequest request) {
105-
return Stream.of(this.toolMethod.getParameters()).map(parameter -> {
106-
// Check if parameter is annotated with @McpProgressToken
107-
if (parameter.isAnnotationPresent(McpProgressToken.class)) {
108-
// Return the progress token from the request
109-
return request != null ? request.progressToken() : null;
110-
}
111-
112-
// Check if parameter is McpMeta type
113-
if (McpMeta.class.isAssignableFrom(parameter.getType())) {
114-
// Return the meta from the request wrapped in McpMeta
115-
return request != null ? new McpMeta(request.meta()) : new McpMeta(null);
116-
}
117-
118-
// Check if parameter is CallToolRequest type
119-
if (CallToolRequest.class.isAssignableFrom(parameter.getType())) {
120-
return request;
121-
}
122-
123-
if (isExchangeOrContextType(parameter.getType())) {
124-
return exchangeOrContext;
125-
}
126-
127-
Object rawArgument = toolInputArguments.get(parameter.getName());
128-
return buildTypedArgument(rawArgument, parameter.getParameterizedType());
129-
}).toArray();
130-
}
131-
132-
/**
133-
* Builds a typed argument from a raw value and type information.
134-
* @param value The raw value
135-
* @param type The target type
136-
* @return The typed argument
137-
*/
138-
protected Object buildTypedArgument(Object value, Type type) {
139-
if (value == null) {
140-
return null;
141-
}
142-
143-
if (type instanceof Class<?>) {
144-
return JsonParser.toTypedObject(value, (Class<?>) type);
145-
}
146-
147-
// For generic types, use the fromJson method that accepts Type
148-
String json = JsonParser.toJson(value);
149-
return JsonParser.fromJson(json, type);
150-
}
151-
15250
/**
15351
* Convert reactive types to Mono<CallToolResult>
15452
* @param result The result from the method invocation
@@ -233,53 +131,23 @@ protected Mono<CallToolResult> convertToCallToolResult(Object result) {
233131
}
234132

235133
/**
236-
* Map individual values to CallToolResult
134+
* Map individual values to CallToolResult This method delegates to the parent class's
135+
* convertValueToCallToolResult method to avoid code duplication.
237136
* @param value The value to map
238137
* @return A CallToolResult representing the mapped value
239138
*/
240139
protected CallToolResult mapValueToCallToolResult(Object value) {
241-
// Return the result if it's already a CallToolResult
242-
if (value instanceof CallToolResult) {
243-
return (CallToolResult) value;
244-
}
245-
246-
Type returnType = this.toolMethod.getGenericReturnType();
247-
248-
if (returnMode == ReturnMode.VOID || returnType == Void.TYPE || returnType == void.class) {
249-
return CallToolResult.builder().addTextContent(JsonParser.toJson("Done")).build();
250-
}
251-
252-
if (this.returnMode == ReturnMode.STRUCTURED) {
253-
String jsonOutput = JsonParser.toJson(value);
254-
Object structuredOutput = JsonParser.fromJson(jsonOutput, MAP_TYPE_REFERENCE);
255-
return CallToolResult.builder().structuredContent(structuredOutput).build();
256-
}
257-
258-
// Default to text output
259-
if (value == null) {
260-
return CallToolResult.builder().addTextContent("null").build();
261-
}
262-
263-
// For string results in TEXT mode, return the string directly without JSON
264-
// serialization
265-
if (value instanceof String) {
266-
return CallToolResult.builder().addTextContent((String) value).build();
267-
}
268-
269-
// For other types, serialize to JSON
270-
return CallToolResult.builder().addTextContent(JsonParser.toJson(value)).build();
140+
return convertValueToCallToolResult(value);
271141
}
272142

273143
/**
274144
* Creates an error result for exceptions that occur during method invocation.
275145
* @param e The exception that occurred
276146
* @return A Mono<CallToolResult> representing the error
277147
*/
278-
protected Mono<CallToolResult> createErrorResult(Exception e) {
279-
return Mono.just(CallToolResult.builder()
280-
.isError(true)
281-
.addTextContent("Error invoking method: %s".formatted(e.getMessage()))
282-
.build());
148+
protected Mono<CallToolResult> createAsyncErrorResult(Exception e) {
149+
Throwable rootCause = findCauseUsingPlainJava(e);
150+
return Mono.just(CallToolResult.builder().isError(true).addTextContent(rootCause.getMessage()).build());
283151
}
284152

285153
/**

0 commit comments

Comments
 (0)