diff --git a/README.md b/README.md
index 9ebb37c..e2bc752 100644
--- a/README.md
+++ b/README.md
@@ -147,6 +147,7 @@ The library automatically filters methods based on the server type and method ch
- **`@McpResource`** - Annotates methods that provide access to resources
- **`@McpTool`** - Annotates methods that implement MCP tools with automatic JSON schema generation
- **`@McpToolParam`** - Annotates tool method parameters with descriptions and requirement specifications
+ - **`@McpToolBody`** - Annotates tool method parameter which is a business object
#### Special Parameters and Annotations
- **`McpSyncRequestContext`** - Special parameter type for synchronous operations that provides a unified interface for accessing MCP request context, including the original request, server exchange (for stateful operations), transport context (for stateless operations), and convenient methods for logging, progress, sampling, and elicitation. This parameter is automatically injected and excluded from JSON schema generation. **Supported in Complete, Prompt, Resource, and Tool methods.**
@@ -452,10 +453,8 @@ public class CalculatorToolProvider {
idempotentHint = true
))
public AreaResult calculateRectangleArea(
- @McpToolParam(description = "Width of the rectangle", required = true) double width,
- @McpToolParam(description = "Height of the rectangle", required = true) double height) {
-
- double area = width * height;
+ @McpToolBody CalculateAreaRequest toolBody) {
+ double area = toolBody.getWidth() * toolBody.getHeight();
return new AreaResult(area, "square units");
}
@@ -509,6 +508,29 @@ public class CalculatorToolProvider {
return actionResult + " with " + (additionalArgs.size() - 1) + " additional parameters";
}
+ public static class CalculateAreaRequest {
+ @McpToolParam(description = "Width of the rectangle", required = true)
+ private double width;
+ @McpToolParam(description = "Height of the rectangle", required = true)
+ private double height;
+
+ public double getWidth() {
+ return width;
+ }
+
+ public void setWidth(double width) {
+ this.width = width;
+ }
+
+ public double getHeight() {
+ return height;
+ }
+
+ public void setHeight(double height) {
+ this.height = height;
+ }
+ }
+
public static class AreaResult {
public double area;
public String unit;
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpProgress.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpProgress.java
index 977b881..35caa2e 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpProgress.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpProgress.java
@@ -16,7 +16,7 @@
*
*
* Methods annotated with this annotation can be used to consume progress messages from
- * MCP servers. The methods takes a single parameter of type {@code ProgressNotification}
+ * MCP servers. The methods take a single parameter of type {@code ProgressNotification}
*
*
*
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpPrompt.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpPrompt.java
index 5002e7f..c4da6c0 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpPrompt.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpPrompt.java
@@ -11,7 +11,7 @@
import java.lang.annotation.Target;
/**
- * Marks a method as a MCP Prompt.
+ * Marks a method as an MCP Prompt.
*
* @author Christian Tzolov
*/
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpToolBody.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpToolBody.java
new file mode 100644
index 0000000..34ee49d
--- /dev/null
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpToolBody.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2025-2025 the original author or authors.
+ */
+
+package org.springaicommunity.mcp.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a method parameter as an MCP body.
+ *
+ * @author lemonj
+ */
+@Target({ ElementType.PARAMETER })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface McpToolBody {
+
+}
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractMcpToolMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractMcpToolMethodCallback.java
index 72314f2..52effba 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractMcpToolMethodCallback.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractMcpToolMethodCallback.java
@@ -28,6 +28,7 @@
import org.springaicommunity.mcp.annotation.McpMeta;
import org.springaicommunity.mcp.annotation.McpProgressToken;
import org.springaicommunity.mcp.annotation.McpTool;
+import org.springaicommunity.mcp.annotation.McpToolBody;
import org.springaicommunity.mcp.context.McpAsyncRequestContext;
import org.springaicommunity.mcp.context.McpRequestContextTypes;
import org.springaicommunity.mcp.context.McpSyncRequestContext;
@@ -107,6 +108,11 @@ protected Object[] buildMethodArguments(T exchangeOrContext, Map
return request != null ? request.progressToken() : null;
}
+ // Check if parameter is annotated with @McpToolBody
+ if (parameter.isAnnotationPresent(McpToolBody.class)) {
+ return buildTypedArgument(toolInputArguments, parameter.getParameterizedType());
+ }
+
// Check if parameter is McpMeta type
if (McpMeta.class.isAssignableFrom(parameter.getType())) {
// Return the meta from the request wrapped in McpMeta
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/utils/JsonSchemaGenerator.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/utils/JsonSchemaGenerator.java
index dacc4c7..b6c59ab 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/utils/JsonSchemaGenerator.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/utils/JsonSchemaGenerator.java
@@ -16,6 +16,8 @@
package org.springaicommunity.mcp.method.tool.utils;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
@@ -26,6 +28,7 @@
import org.springaicommunity.mcp.annotation.McpMeta;
import org.springaicommunity.mcp.annotation.McpProgressToken;
+import org.springaicommunity.mcp.annotation.McpToolBody;
import org.springaicommunity.mcp.annotation.McpToolParam;
import org.springaicommunity.mcp.context.McpAsyncRequestContext;
import org.springaicommunity.mcp.context.McpSyncRequestContext;
@@ -102,14 +105,15 @@ public static String generateForMethodInput(Method method) {
private static String internalGenerateFromMethodArguments(Method method) {
// Check if method has CallToolRequest parameter
boolean hasCallToolRequestParam = Arrays.stream(method.getParameterTypes())
- .anyMatch(type -> CallToolRequest.class.isAssignableFrom(type));
+ .anyMatch(CallToolRequest.class::isAssignableFrom);
// If method has CallToolRequest, return minimal schema
+ Parameter[] parameters = method.getParameters();
if (hasCallToolRequestParam) {
// Check if there are other parameters besides CallToolRequest, exchange
// types,
// @McpProgressToken annotated parameters, and McpMeta parameters
- boolean hasOtherParams = Arrays.stream(method.getParameters()).anyMatch(param -> {
+ boolean hasOtherParams = Arrays.stream(parameters).anyMatch(param -> {
Class> type = param.getType();
return !McpSyncRequestContext.class.isAssignableFrom(type)
&& !McpAsyncRequestContext.class.isAssignableFrom(type)
@@ -136,10 +140,10 @@ private static String internalGenerateFromMethodArguments(Method method) {
ObjectNode properties = schema.putObject("properties");
List required = new ArrayList<>();
- for (int i = 0; i < method.getParameterCount(); i++) {
- Parameter parameter = method.getParameters()[i];
+ for (int i = 0; i < parameters.length; i++) {
+ Parameter parameter = parameters[i];
String parameterName = parameter.getName();
- Type parameterType = method.getGenericParameterTypes()[i];
+ Type parameterType = parameter.getParameterizedType();
// Skip parameters annotated with @McpProgressToken
if (parameter.isAnnotationPresent(McpProgressToken.class)) {
@@ -161,11 +165,32 @@ private static String internalGenerateFromMethodArguments(Method method) {
continue;
}
- if (isMethodParameterRequired(method, i)) {
+ // support McpToolBody
+ if (parameter.isAnnotationPresent(McpToolBody.class)) {
+ Class> paramType = (Class>) parameter.getParameterizedType();
+ Field[] declaredFields = paramType.getDeclaredFields();
+
+ // handle field
+ for (Field field : declaredFields) {
+ if (isElementRequired(field)) {
+ required.add(field.getName());
+ }
+
+ ObjectNode parameterNode = SUBTYPE_SCHEMA_GENERATOR.generateSchema(field.getGenericType());
+ String parameterDescription = getMethodParameterDescription(field);
+ if (Utils.hasText(parameterDescription)) {
+ parameterNode.put("description", parameterDescription);
+ }
+ properties.set(field.getName(), parameterNode);
+ }
+ continue;
+ }
+
+ if (isElementRequired(parameter)) {
required.add(parameterName);
}
ObjectNode parameterNode = SUBTYPE_SCHEMA_GENERATOR.generateSchema(parameterType);
- String parameterDescription = getMethodParameterDescription(method, i);
+ String parameterDescription = getMethodParameterDescription(parameter);
if (Utils.hasText(parameterDescription)) {
parameterNode.put("description", parameterDescription);
}
@@ -205,26 +230,24 @@ public static boolean hasCallToolRequestParameter(Method method) {
return Arrays.stream(method.getParameterTypes()).anyMatch(type -> CallToolRequest.class.isAssignableFrom(type));
}
- private static boolean isMethodParameterRequired(Method method, int index) {
- Parameter parameter = method.getParameters()[index];
-
- var toolParamAnnotation = parameter.getAnnotation(McpToolParam.class);
+ private static boolean isElementRequired(AnnotatedElement element) {
+ var toolParamAnnotation = element.getAnnotation(McpToolParam.class);
if (toolParamAnnotation != null) {
return toolParamAnnotation.required();
}
- var propertyAnnotation = parameter.getAnnotation(JsonProperty.class);
+ var propertyAnnotation = element.getAnnotation(JsonProperty.class);
if (propertyAnnotation != null) {
return propertyAnnotation.required();
}
- var schemaAnnotation = parameter.getAnnotation(Schema.class);
+ var schemaAnnotation = element.getAnnotation(Schema.class);
if (schemaAnnotation != null) {
return schemaAnnotation.requiredMode() == Schema.RequiredMode.REQUIRED
|| schemaAnnotation.requiredMode() == Schema.RequiredMode.AUTO || schemaAnnotation.required();
}
- var nullableAnnotation = parameter.getAnnotation(Nullable.class);
+ var nullableAnnotation = element.getAnnotation(Nullable.class);
if (nullableAnnotation != null) {
return false;
}
@@ -232,25 +255,22 @@ private static boolean isMethodParameterRequired(Method method, int index) {
return PROPERTY_REQUIRED_BY_DEFAULT;
}
- private static String getMethodParameterDescription(Method method, int index) {
- Parameter parameter = method.getParameters()[index];
-
- var toolParamAnnotation = parameter.getAnnotation(McpToolParam.class);
+ private static String getMethodParameterDescription(AnnotatedElement element) {
+ McpToolParam toolParamAnnotation = element.getAnnotation(McpToolParam.class);
if (toolParamAnnotation != null && Utils.hasText(toolParamAnnotation.description())) {
return toolParamAnnotation.description();
}
-
- var jacksonAnnotation = parameter.getAnnotation(JsonPropertyDescription.class);
- if (jacksonAnnotation != null && Utils.hasText(jacksonAnnotation.value())) {
- return jacksonAnnotation.value();
- }
-
- var schemaAnnotation = parameter.getAnnotation(Schema.class);
- if (schemaAnnotation != null && Utils.hasText(schemaAnnotation.description())) {
- return schemaAnnotation.description();
+ else {
+ JsonPropertyDescription jacksonAnnotation = element.getAnnotation(JsonPropertyDescription.class);
+ if (jacksonAnnotation != null && Utils.hasText(jacksonAnnotation.value())) {
+ return jacksonAnnotation.value();
+ }
+ else {
+ Schema schemaAnnotation = element.getAnnotation(Schema.class);
+ return schemaAnnotation != null && Utils.hasText(schemaAnnotation.description())
+ ? schemaAnnotation.description() : null;
+ }
}
-
- return null;
}
}