Skip to content
Merged
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
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,35 @@ public class CalculatorToolProvider {
}).subscribeOn(Schedulers.boundedElastic());
}

// Tool with CallToolRequest parameter for dynamic schema support
@McpTool(name = "dynamic-processor", description = "Process data with dynamic schema")
public CallToolResult processDynamic(CallToolRequest request) {
// Access the full request including dynamic schema
Map<String, Object> args = request.arguments();

// Process based on runtime schema
String result = "Processed " + args.size() + " arguments dynamically";

return CallToolResult.builder()
.addTextContent(result)
.build();
}

// Tool with mixed parameters - typed and CallToolRequest
@McpTool(name = "hybrid-processor", description = "Process with both typed and dynamic parameters")
public String processHybrid(
@McpToolParam(description = "Action to perform", required = true) String action,
CallToolRequest request) {

// Use typed parameter
String actionResult = "Action: " + action;

// Also access additional dynamic arguments
Map<String, Object> additionalArgs = request.arguments();

return actionResult + " with " + (additionalArgs.size() - 1) + " additional parameters";
}

public static class AreaResult {
public double area;
public String unit;
Expand All @@ -433,6 +462,54 @@ public class CalculatorToolProvider {
}
```

#### CallToolRequest Support

The library supports special `CallToolRequest` parameters in tool methods, enabling dynamic schema handling at runtime. This is useful when you need to:

- Accept tools with schemas defined at runtime
- Process requests where the input structure isn't known at compile time
- Build flexible tools that adapt to different input schemas

When a tool method includes a `CallToolRequest` parameter:
- The parameter receives the complete tool request including all arguments
- For methods with only `CallToolRequest`, a minimal schema is generated
- For methods with mixed parameters, only non-`CallToolRequest` parameters are included in the schema
- The `CallToolRequest` parameter is automatically injected and doesn't appear in the tool's input schema

Example usage:

```java
// Tool that accepts any schema at runtime
@McpTool(name = "flexible-tool")
public CallToolResult processAnySchema(CallToolRequest request) {
Map<String, Object> args = request.arguments();
// Process based on whatever schema was provided at runtime
return CallToolResult.success(processedResult);
}

// Tool with both typed and dynamic parameters
@McpTool(name = "mixed-tool")
public String processMixed(
@McpToolParam("operation") String operation,
@McpToolParam("count") int count,
CallToolRequest request) {

// Use typed parameters for known fields
String result = operation + " x " + count;

// Access any additional fields from the request
Map<String, Object> allArgs = request.arguments();

return result;
}
```

This feature works with all tool callback types:
- `SyncMcpToolMethodCallback` - Synchronous with server exchange
- `AsyncMcpToolMethodCallback` - Asynchronous with server exchange
- `SyncStatelessMcpToolMethodCallback` - Synchronous stateless
- `AsyncStatelessMcpToolMethodCallback` - Asynchronous stateless

### Async Tool Example

```java
Expand Down Expand Up @@ -1168,6 +1245,7 @@ public class McpConfig {
- **Comprehensive validation** - Ensures method signatures are compatible with MCP operations
- **URI template support** - Powerful URI template handling for resource and completion operations
- **Tool support with automatic JSON schema generation** - Create MCP tools with automatic input/output schema generation from method signatures
- **Dynamic schema support via CallToolRequest** - Tools can accept `CallToolRequest` parameters to handle dynamic schemas at runtime
- **Logging consumer support** - Handle logging message notifications from MCP servers
- **Sampling support** - Handle sampling requests from MCP servers
- **Spring integration** - Seamless integration with Spring Framework and Spring AI, including support for both stateful and stateless operations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,31 @@ protected Object callMethod(Object[] methodArguments) {
* @return An array of method arguments
*/
protected Object[] buildMethodArguments(T exchangeOrContext, Map<String, Object> toolInputArguments) {
return buildMethodArguments(exchangeOrContext, toolInputArguments, null);
}

/**
* Builds the method arguments from the context, tool input arguments, and optionally
* the full request.
* @param exchangeOrContext The exchange or context object (e.g.,
* McpAsyncServerExchange or McpTransportContext)
* @param toolInputArguments The input arguments from the tool request
* @param request The full CallToolRequest (optional, can be null)
* @return An array of method arguments
*/
protected Object[] buildMethodArguments(T exchangeOrContext, Map<String, Object> toolInputArguments,
CallToolRequest request) {
return Stream.of(this.toolMethod.getParameters()).map(parameter -> {
Object rawArgument = toolInputArguments.get(parameter.getName());
// Check if parameter is CallToolRequest type
if (CallToolRequest.class.isAssignableFrom(parameter.getType())) {
return request;
}

if (isExchangeOrContextType(parameter.getType())) {
return exchangeOrContext;
}

Object rawArgument = toolInputArguments.get(parameter.getName());
return buildTypedArgument(rawArgument, parameter.getParameterizedType());
}).toArray();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,31 @@ protected Object callMethod(Object[] methodArguments) {
* @return An array of method arguments
*/
protected Object[] buildMethodArguments(T exchangeOrContext, Map<String, Object> toolInputArguments) {
return buildMethodArguments(exchangeOrContext, toolInputArguments, null);
}

/**
* Builds the method arguments from the context, tool input arguments, and optionally
* the full request.
* @param exchangeOrContext The exchange or context object (e.g.,
* McpSyncServerExchange or McpTransportContext)
* @param toolInputArguments The input arguments from the tool request
* @param request The full CallToolRequest (optional, can be null)
* @return An array of method arguments
*/
protected Object[] buildMethodArguments(T exchangeOrContext, Map<String, Object> toolInputArguments,
CallToolRequest request) {
return Stream.of(this.toolMethod.getParameters()).map(parameter -> {
Object rawArgument = toolInputArguments.get(parameter.getName());
// Check if parameter is CallToolRequest type
if (CallToolRequest.class.isAssignableFrom(parameter.getType())) {
return request;
}

if (isExchangeOrContextType(parameter.getType())) {
return exchangeOrContext;
}

Object rawArgument = toolInputArguments.get(parameter.getName());
return buildTypedArgument(rawArgument, parameter.getParameterizedType());
}).toArray();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ public Mono<CallToolResult> apply(McpAsyncServerExchange exchange, CallToolReque

return validateRequest(request).then(Mono.defer(() -> {
try {
// Build arguments for the method call
Object[] args = this.buildMethodArguments(exchange, request.arguments());
// Build arguments for the method call, passing the full request for
// CallToolRequest parameter support
Object[] args = this.buildMethodArguments(exchange, request.arguments(), request);

// Invoke the method
Object result = this.callMethod(args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public Mono<CallToolResult> apply(McpTransportContext mcpTransportContext, CallT
return validateRequest(request).then(Mono.defer(() -> {
try {
// Build arguments for the method call
Object[] args = this.buildMethodArguments(mcpTransportContext, request.arguments());
Object[] args = this.buildMethodArguments(mcpTransportContext, request.arguments(), request);

// Invoke the method
Object result = this.callMethod(args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ public CallToolResult apply(McpSyncServerExchange exchange, CallToolRequest requ
validateRequest(request);

try {
// Build arguments for the method call
Object[] args = this.buildMethodArguments(exchange, request.arguments());
// Build arguments for the method call, passing the full request for
// CallToolRequest parameter support
Object[] args = this.buildMethodArguments(exchange, request.arguments(), request);

// Invoke the method
Object result = this.callMethod(args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public CallToolResult apply(McpTransportContext mcpTransportContext, CallToolReq

try {
// Build arguments for the method call
Object[] args = this.buildMethodArguments(mcpTransportContext, callToolRequest.arguments());
Object[] args = this.buildMethodArguments(mcpTransportContext, callToolRequest.arguments(),
callToolRequest);

// Invoke the method
Object result = this.callMethod(args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

Expand All @@ -41,6 +42,7 @@

import io.modelcontextprotocol.server.McpAsyncServerExchange;
import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.Utils;
import io.swagger.v3.oas.annotations.media.Schema;
Expand Down Expand Up @@ -93,6 +95,30 @@ 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));

// If method has CallToolRequest, return minimal schema
if (hasCallToolRequestParam) {
// Check if there are other parameters besides CallToolRequest and exchange
// types
boolean hasOtherParams = Arrays.stream(method.getParameters()).anyMatch(param -> {
Class<?> type = param.getType();
return !CallToolRequest.class.isAssignableFrom(type)
&& !McpSyncServerExchange.class.isAssignableFrom(type)
&& !McpAsyncServerExchange.class.isAssignableFrom(type);
});

// If only CallToolRequest (and possibly exchange), return empty schema
if (!hasOtherParams) {
ObjectNode schema = JsonParser.getObjectMapper().createObjectNode();
schema.put("type", "object");
schema.putObject("properties");
schema.putArray("required");
return schema.toPrettyString();
}
}

ObjectNode schema = JsonParser.getObjectMapper().createObjectNode();
schema.put("$schema", SchemaVersion.DRAFT_2020_12.getIdentifier());
Expand All @@ -104,11 +130,15 @@ private static String internalGenerateFromMethodArguments(Method method) {
for (int i = 0; i < method.getParameterCount(); i++) {
String parameterName = method.getParameters()[i].getName();
Type parameterType = method.getGenericParameterTypes()[i];

// Skip special parameter types
if (parameterType instanceof Class<?> parameterClass
&& (ClassUtils.isAssignable(McpSyncServerExchange.class, parameterClass)
|| ClassUtils.isAssignable(McpAsyncServerExchange.class, parameterClass))) {
|| ClassUtils.isAssignable(McpAsyncServerExchange.class, parameterClass)
|| ClassUtils.isAssignable(CallToolRequest.class, parameterClass))) {
continue;
}

if (isMethodParameterRequired(method, i)) {
required.add(parameterName);
}
Expand Down Expand Up @@ -143,6 +173,15 @@ private static String internalGenerateFromClass(Class<?> clazz) {
return jsonSchema.toPrettyString();
}

/**
* Check if a method has a CallToolRequest parameter.
* @param method The method to check
* @return true if the method has a CallToolRequest parameter, false otherwise
*/
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];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.springaicommunity.mcp.provider;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction;
import java.util.stream.Stream;
Expand Down Expand Up @@ -78,7 +79,21 @@ public List<SyncToolSpecification> getToolSpecifications() {

String toolDescription = toolAnnotation.description();

String inputSchema = JsonSchemaGenerator.generateForMethodInput(mcpToolMethod);
// Check if method has CallToolRequest parameter
boolean hasCallToolRequestParam = Arrays.stream(mcpToolMethod.getParameterTypes())
.anyMatch(type -> CallToolRequest.class.isAssignableFrom(type));

String inputSchema;
if (hasCallToolRequestParam) {
// For methods with CallToolRequest, generate minimal schema or
// use the one from the request
// The schema generation will handle this appropriately
inputSchema = JsonSchemaGenerator.generateForMethodInput(mcpToolMethod);
logger.debug("Tool method '{}' uses CallToolRequest parameter, using minimal schema", toolName);
}
else {
inputSchema = JsonSchemaGenerator.generateForMethodInput(mcpToolMethod);
}

var toolBuilder = McpSchema.Tool.builder()
.name(toolName)
Expand Down
Loading