Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand Down Expand Up @@ -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");
}

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*
* <p>
* 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}
*
*
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -107,6 +108,11 @@ protected Object[] buildMethodArguments(T exchangeOrContext, Map<String, Object>
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -136,10 +140,10 @@ private static String internalGenerateFromMethodArguments(Method method) {
ObjectNode properties = schema.putObject("properties");
List<String> 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)) {
Expand All @@ -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);
}
Expand Down Expand Up @@ -205,52 +230,47 @@ 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;
}

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;
}

}