Skip to content

Commit 28420d4

Browse files
refactor: unify error handling across MCP component types
Centralize invocation and parameter conversion via ComponentInvocationSupport so tool, prompt, resource, and completion share the same sanitized error contract. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8c6afcd commit 28420d4

6 files changed

Lines changed: 131 additions & 30 deletions

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package com.github.thought2code.mcp.annotated.reflect;
2+
3+
import com.github.thought2code.mcp.annotated.enums.McpServerError;
4+
import com.github.thought2code.mcp.annotated.exception.McpServerException;
5+
import com.github.thought2code.mcp.annotated.server.converter.AbstractParameterConverter;
6+
import io.modelcontextprotocol.spec.McpSchema;
7+
import java.lang.reflect.Method;
8+
import java.util.List;
9+
import java.util.Map;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
13+
/**
14+
* Shared invocation flow for MCP server components (tool, prompt, resource, completion).
15+
*
16+
* <p>Centralizes parameter conversion and method invocation so all component types apply the same
17+
* sanitized error contract from {@link MethodInvoker}.
18+
*
19+
* @author codeboyzhou
20+
*/
21+
public final class ComponentInvocationSupport {
22+
23+
private static final Logger log = LoggerFactory.getLogger(ComponentInvocationSupport.class);
24+
25+
private ComponentInvocationSupport() {}
26+
27+
/**
28+
* Converts request arguments and invokes the target method.
29+
*
30+
* @param instance component instance
31+
* @param method target method
32+
* @param parameterConverter converter for annotated parameters
33+
* @param arguments request arguments
34+
* @return invocation result, including sanitized errors from conversion or method failures
35+
*/
36+
public static Invocation invokeWithParameters(
37+
Object instance,
38+
Method method,
39+
AbstractParameterConverter<?> parameterConverter,
40+
Map<String, Object> arguments) {
41+
MethodMetadata metadata = MethodMetadata.of(method);
42+
try {
43+
List<Object> params = parameterConverter.convertAll(metadata.getParameters(), arguments);
44+
return MethodInvoker.invoke(instance, method, metadata, params);
45+
} catch (RuntimeException e) {
46+
return parameterConversionFailure(e);
47+
}
48+
}
49+
50+
/**
51+
* Invokes a no-argument component method.
52+
*
53+
* @param instance component instance
54+
* @param method target method
55+
* @return invocation result
56+
*/
57+
public static Invocation invoke(Object instance, Method method) {
58+
return MethodInvoker.invoke(instance, method, MethodMetadata.of(method));
59+
}
60+
61+
/**
62+
* Invokes a completion handler method.
63+
*
64+
* @param instance component instance
65+
* @param method target method
66+
* @param argument completion argument from the request
67+
* @return invocation result
68+
*/
69+
public static Invocation invoke(
70+
Object instance, Method method, McpSchema.CompleteRequest.CompleteArgument argument) {
71+
MethodMetadata metadata = MethodMetadata.of(method);
72+
return MethodInvoker.invoke(instance, method, metadata, argument);
73+
}
74+
75+
private static Invocation parameterConversionFailure(RuntimeException e) {
76+
log.error("Parameter conversion failed", e);
77+
return Invocation.builder()
78+
.result(McpServerError.METHOD_INVOCATION_ERROR.toString())
79+
.isError(true)
80+
.build();
81+
}
82+
83+
/**
84+
* Throws {@link McpServerException} when the invocation failed.
85+
*
86+
* @param invocation invocation to check
87+
*/
88+
public static void throwIfError(Invocation invocation) {
89+
if (invocation.isError()) {
90+
throw new McpServerException(invocation.result().toString());
91+
}
92+
}
93+
94+
/**
95+
* Returns the invocation result when successful.
96+
*
97+
* @param invocation invocation to check
98+
* @param type expected result type
99+
* @param <T> result type
100+
* @return typed invocation result
101+
* @throws McpServerException when the invocation failed
102+
*/
103+
@SuppressWarnings("unchecked")
104+
public static <T> T requireResult(Invocation invocation, Class<T> type) {
105+
throwIfError(invocation);
106+
return (T) invocation.result();
107+
}
108+
}

src/main/java/com/github/thought2code/mcp/annotated/reflect/Invocation.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ public static Builder builder() {
1717
return new Builder();
1818
}
1919

20+
/** Returns the invocation result as text. */
21+
public String asText() {
22+
return result.toString();
23+
}
24+
2025
/**
2126
* This class implements the builder pattern for creating a new instance of {@code
2227
* InvocationResult}.

src/main/java/com/github/thought2code/mcp/annotated/server/component/McpServerCompletion.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
import com.github.thought2code.mcp.annotated.annotation.McpPromptCompletion;
55
import com.github.thought2code.mcp.annotated.annotation.McpResourceCompletion;
66
import com.github.thought2code.mcp.annotated.exception.McpServerComponentRegistrationException;
7+
import com.github.thought2code.mcp.annotated.reflect.ComponentInvocationSupport;
78
import com.github.thought2code.mcp.annotated.reflect.Invocation;
8-
import com.github.thought2code.mcp.annotated.reflect.MethodInvoker;
9-
import com.github.thought2code.mcp.annotated.reflect.MethodMetadata;
109
import io.modelcontextprotocol.server.McpServerFeatures;
1110
import io.modelcontextprotocol.spec.McpSchema;
1211
import java.lang.reflect.Method;
@@ -180,10 +179,9 @@ private static McpServerFeatures.AsyncCompletionSpecification fromAsync(
180179
private static McpSchema.CompleteResult invoke(
181180
Object instance, Method method, McpSchema.CompleteRequest request) {
182181

183-
MethodMetadata metadata = MethodMetadata.of(method);
184-
McpSchema.CompleteRequest.CompleteArgument argument = request.argument();
185-
Invocation invocation = MethodInvoker.invoke(instance, method, metadata, argument);
186-
McpCompleteCompletion completion = (McpCompleteCompletion) invocation.result();
182+
Invocation invocation = ComponentInvocationSupport.invoke(instance, method, request.argument());
183+
McpCompleteCompletion completion =
184+
ComponentInvocationSupport.requireResult(invocation, McpCompleteCompletion.class);
187185
return new McpSchema.CompleteResult(
188186
new McpSchema.CompleteResult.CompleteCompletion(
189187
completion.values(), completion.total(), completion.hasMore()));

src/main/java/com/github/thought2code/mcp/annotated/server/component/McpServerPrompt.java

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
import com.github.thought2code.mcp.annotated.annotation.McpPrompt;
55
import com.github.thought2code.mcp.annotated.annotation.McpPromptParam;
66
import com.github.thought2code.mcp.annotated.enums.ServerType;
7+
import com.github.thought2code.mcp.annotated.reflect.ComponentInvocationSupport;
78
import com.github.thought2code.mcp.annotated.reflect.Invocation;
8-
import com.github.thought2code.mcp.annotated.reflect.MethodInvoker;
9-
import com.github.thought2code.mcp.annotated.reflect.MethodMetadata;
109
import com.github.thought2code.mcp.annotated.server.converter.McpPromptParameterConverter;
1110
import com.github.thought2code.mcp.annotated.util.JacksonHelper;
1211
import com.github.thought2code.mcp.annotated.util.StringHelper;
@@ -18,7 +17,6 @@
1817
import java.lang.reflect.Parameter;
1918
import java.util.ArrayList;
2019
import java.util.List;
21-
import java.util.Map;
2220
import org.slf4j.Logger;
2321
import org.slf4j.LoggerFactory;
2422
import reactor.core.publisher.Mono;
@@ -169,14 +167,11 @@ private McpSchema.GetPromptResult invoke(
169167

170168
log.debug("Handling MCP GetPromptRequest: {}", JacksonHelper.toJsonString(request));
171169

172-
MethodMetadata metadata = MethodMetadata.of(method);
173-
Parameter[] parameters = metadata.getParameters();
174-
Map<String, Object> arguments = request.arguments();
175-
List<Object> params = parameterConverter.convertAll(parameters, arguments);
176-
Invocation invocation = MethodInvoker.invoke(instance, method, metadata, params);
170+
Invocation invocation =
171+
ComponentInvocationSupport.invokeWithParameters(
172+
instance, method, parameterConverter, request.arguments());
177173

178-
McpSchema.Content content =
179-
McpSchema.TextContent.builder(invocation.result().toString()).build();
174+
McpSchema.Content content = McpSchema.TextContent.builder(invocation.asText()).build();
180175
McpSchema.PromptMessage message =
181176
McpSchema.PromptMessage.builder(McpSchema.Role.USER, content).build();
182177
McpSchema.GetPromptResult getPromptResult =

src/main/java/com/github/thought2code/mcp/annotated/server/component/McpServerResource.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
import com.github.thought2code.mcp.annotated.McpApplicationContext;
44
import com.github.thought2code.mcp.annotated.annotation.McpResource;
55
import com.github.thought2code.mcp.annotated.enums.ServerType;
6+
import com.github.thought2code.mcp.annotated.reflect.ComponentInvocationSupport;
67
import com.github.thought2code.mcp.annotated.reflect.Invocation;
7-
import com.github.thought2code.mcp.annotated.reflect.MethodInvoker;
8-
import com.github.thought2code.mcp.annotated.reflect.MethodMetadata;
98
import com.github.thought2code.mcp.annotated.util.JacksonHelper;
109
import com.github.thought2code.mcp.annotated.util.StringHelper;
1110
import io.modelcontextprotocol.server.McpAsyncServer;
@@ -153,11 +152,10 @@ private McpSchema.ReadResourceResult invoke(
153152

154153
log.debug("Handling ReadResourceResult request: {}", JacksonHelper.toJsonString(resource));
155154

156-
MethodMetadata metadata = MethodMetadata.of(method);
157-
Invocation invocation = MethodInvoker.invoke(instance, method, metadata);
155+
Invocation invocation = ComponentInvocationSupport.invoke(instance, method);
158156
final String uri = resource.uri();
159157
final String mimeType = resource.mimeType();
160-
final String text = invocation.result().toString();
158+
final String text = invocation.asText();
161159
McpSchema.ResourceContents contents =
162160
McpSchema.TextResourceContents.builder(uri, text).mimeType(mimeType).build();
163161
McpSchema.ReadResourceResult readResourceResult =

src/main/java/com/github/thought2code/mcp/annotated/server/component/McpServerTool.java

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77
import com.github.thought2code.mcp.annotated.annotation.McpToolParam;
88
import com.github.thought2code.mcp.annotated.enums.JavaTypeToJsonSchemaMapper;
99
import com.github.thought2code.mcp.annotated.enums.ServerType;
10+
import com.github.thought2code.mcp.annotated.reflect.ComponentInvocationSupport;
1011
import com.github.thought2code.mcp.annotated.reflect.Invocation;
11-
import com.github.thought2code.mcp.annotated.reflect.MethodInvoker;
12-
import com.github.thought2code.mcp.annotated.reflect.MethodMetadata;
1312
import com.github.thought2code.mcp.annotated.server.McpStructuredContent;
1413
import com.github.thought2code.mcp.annotated.server.converter.McpToolParameterConverter;
1514
import com.github.thought2code.mcp.annotated.util.JacksonHelper;
@@ -180,17 +179,15 @@ private McpSchema.CallToolResult invoke(
180179

181180
log.debug("Handling MCP CallToolRequest: {}", JacksonHelper.toJsonString(request));
182181

183-
MethodMetadata metadata = MethodMetadata.of(method);
184-
Parameter[] parameters = metadata.getParameters();
185-
Map<String, Object> arguments = request.arguments();
186-
List<Object> params = parameterConverter.convertAll(parameters, arguments);
187-
Invocation invocation = MethodInvoker.invoke(instance, method, metadata, params);
182+
Invocation invocation =
183+
ComponentInvocationSupport.invokeWithParameters(
184+
instance, method, parameterConverter, request.arguments());
188185

189186
Object result = invocation.result();
190-
String textContent = result.toString();
187+
String textContent = invocation.asText();
191188
Object structuredContent = Map.of();
192189

193-
if (result instanceof McpStructuredContent mcpStructuredContent) {
190+
if (!invocation.isError() && result instanceof McpStructuredContent mcpStructuredContent) {
194191
textContent = mcpStructuredContent.asTextContent();
195192
structuredContent = mcpStructuredContent;
196193
}

0 commit comments

Comments
 (0)