|
16 | 16 |
|
17 | 17 | package org.springaicommunity.mcp.method.tool; |
18 | 18 |
|
19 | | -import java.lang.reflect.InvocationTargetException; |
20 | 19 | import java.lang.reflect.Method; |
21 | | -import java.lang.reflect.Type; |
22 | | -import java.util.Map; |
23 | | -import java.util.stream.Stream; |
24 | 20 |
|
| 21 | +import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; |
| 22 | +import io.modelcontextprotocol.spec.McpSchema.CallToolResult; |
25 | 23 | import org.reactivestreams.Publisher; |
26 | | -import org.springaicommunity.mcp.annotation.McpMeta; |
27 | | -import org.springaicommunity.mcp.annotation.McpProgressToken; |
28 | 24 | import org.springaicommunity.mcp.annotation.McpTool; |
29 | 25 | 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; |
35 | 26 | import reactor.core.publisher.Flux; |
36 | 27 | import reactor.core.publisher.Mono; |
37 | 28 |
|
|
46 | 37 | * McpTransportContext) |
47 | 38 | * @author Christian Tzolov |
48 | 39 | */ |
49 | | -public abstract class AbstractAsyncMcpToolMethodCallback<T> { |
| 40 | +public abstract class AbstractAsyncMcpToolMethodCallback<T> extends AbstractMcpToolMethodCallback<T> { |
50 | 41 |
|
51 | 42 | protected final Class<? extends Throwable> toolCallExceptionClass; |
52 | 43 |
|
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 | | - |
63 | 44 | protected AbstractAsyncMcpToolMethodCallback(ReturnMode returnMode, Method toolMethod, Object toolObject, |
64 | 45 | Class<? extends Throwable> toolCallExceptionClass) { |
65 | | - this.toolMethod = toolMethod; |
66 | | - this.toolObject = toolObject; |
67 | | - this.returnMode = returnMode; |
| 46 | + super(returnMode, toolMethod, toolObject); |
68 | 47 | this.toolCallExceptionClass = toolCallExceptionClass; |
69 | 48 | } |
70 | 49 |
|
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 | | - |
152 | 50 | /** |
153 | 51 | * Convert reactive types to Mono<CallToolResult> |
154 | 52 | * @param result The result from the method invocation |
@@ -233,53 +131,23 @@ protected Mono<CallToolResult> convertToCallToolResult(Object result) { |
233 | 131 | } |
234 | 132 |
|
235 | 133 | /** |
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. |
237 | 136 | * @param value The value to map |
238 | 137 | * @return A CallToolResult representing the mapped value |
239 | 138 | */ |
240 | 139 | 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); |
271 | 141 | } |
272 | 142 |
|
273 | 143 | /** |
274 | 144 | * Creates an error result for exceptions that occur during method invocation. |
275 | 145 | * @param e The exception that occurred |
276 | 146 | * @return A Mono<CallToolResult> representing the error |
277 | 147 | */ |
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()); |
283 | 151 | } |
284 | 152 |
|
285 | 153 | /** |
|
0 commit comments