Skip to content

Commit 3b22fab

Browse files
authored
feat: Add comprehensive MCP tool support with annotations and JSON schema generation (#9)
- Add @mcptool and @McpToolParam annotations for defining MCP tools - Implement AsyncMcpToolProvider and SyncMcpToolProvider for tool management - Add tool method callbacks for both synchronous and asynchronous operations - Integrate JSON schema generation using victools jsonschema library - Add utility classes for JSON parsing, reactive types, and class introspection - Update Spring integration classes to support tool specifications - Add comprehensive test coverage for all tool-related functionality - Update dependencies to include Jackson, Swagger annotations, and schema generation - Update README documentation BREAKING: Removes incorrectly placed async sampling components from sync provider Resolves #2 Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
1 parent a421fd6 commit 3b22fab

24 files changed

+4784
-56
lines changed

README.md

Lines changed: 186 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ To use the MCP Annotations core module in your project, add the following depend
3636
<dependency>
3737
<groupId>org.springaicommunity</groupId>
3838
<artifactId>mcp-annotations</artifactId>
39-
<version>0.1.0</version>
39+
<version>0.2.0-SNAPSHOT</version>
4040
</dependency>
4141
```
4242

@@ -48,7 +48,7 @@ To use the Spring integration module, add the following dependency:
4848
<dependency>
4949
<groupId>corg.springaicommunity</groupId>
5050
<artifactId>spring-ai-mcp-annotations</artifactId>
51-
<version>0.1.0</version>
51+
<version>0.2.0-SNAPSHOT</version>
5252
</dependency>
5353
```
5454

@@ -89,8 +89,9 @@ The core module provides a set of annotations and callback implementations for p
8989
1. **Complete** - For auto-completion functionality in prompts and URI templates
9090
2. **Prompt** - For generating prompt messages
9191
3. **Resource** - For accessing resources via URI templates
92-
4. **Logging Consumer** - For handling logging message notifications
93-
5. **Sampling** - For handling sampling requests
92+
4. **Tool** - For implementing MCP tools with automatic JSON schema generation
93+
5. **Logging Consumer** - For handling logging message notifications
94+
6. **Sampling** - For handling sampling requests
9495

9596
Each operation type has both synchronous and asynchronous implementations, allowing for flexible integration with different application architectures.
9697

@@ -105,6 +106,8 @@ The Spring integration module provides seamless integration with Spring AI and S
105106
- **`@McpComplete`** - Annotates methods that provide completion functionality for prompts or URI templates
106107
- **`@McpPrompt`** - Annotates methods that generate prompt messages
107108
- **`@McpResource`** - Annotates methods that provide access to resources
109+
- **`@McpTool`** - Annotates methods that implement MCP tools with automatic JSON schema generation
110+
- **`@McpToolParam`** - Annotates tool method parameters with descriptions and requirement specifications
108111
- **`@McpLoggingConsumer`** - Annotates methods that handle logging message notifications from MCP servers
109112
- **`@McpSampling`** - Annotates methods that handle sampling requests from MCP servers
110113
- **`@McpArg`** - Annotates method parameters as MCP arguments
@@ -133,6 +136,10 @@ The modules provide callback implementations for each operation type:
133136
- `SyncMcpLoggingConsumerMethodCallback` - Synchronous implementation
134137
- `AsyncMcpLoggingConsumerMethodCallback` - Asynchronous implementation using Reactor's Mono
135138

139+
#### Tool
140+
- `SyncMcpToolMethodCallback` - Synchronous implementation for tool method callbacks
141+
- `AsyncMcpToolMethodCallback` - Asynchronous implementation using Reactor's Mono
142+
136143
#### Sampling
137144
- `AbstractMcpSamplingMethodCallback` - Base class for sampling method callbacks
138145
- `SyncMcpSamplingMethodCallback` - Synchronous implementation
@@ -145,6 +152,8 @@ The project includes provider classes that scan for annotated methods and create
145152
- `SyncMcpCompletionProvider` - Processes `@McpComplete` annotations for synchronous operations
146153
- `SyncMcpPromptProvider` - Processes `@McpPrompt` annotations for synchronous operations
147154
- `SyncMcpResourceProvider` - Processes `@McpResource` annotations for synchronous operations
155+
- `SyncMcpToolProvider` - Processes `@McpTool` annotations for synchronous operations
156+
- `AsyncMcpToolProvider` - Processes `@McpTool` annotations for asynchronous operations
148157
- `SyncMcpLoggingConsumerProvider` - Processes `@McpLoggingConsumer` annotations for synchronous operations
149158
- `AsyncMcpLoggingConsumerProvider` - Processes `@McpLoggingConsumer` annotations for asynchronous operations
150159
- `SyncMcpSamplingProvider` - Processes `@McpSampling` annotations for synchronous operations
@@ -325,6 +334,166 @@ public class MyResourceProvider {
325334
}
326335
```
327336

337+
### Tool Example
338+
339+
```java
340+
public class CalculatorToolProvider {
341+
342+
@McpTool(name = "add", description = "Add two numbers together")
343+
public int add(
344+
@McpToolParam(description = "First number to add", required = true) int a,
345+
@McpToolParam(description = "Second number to add", required = true) int b) {
346+
return a + b;
347+
}
348+
349+
@McpTool(name = "multiply", description = "Multiply two numbers")
350+
public double multiply(
351+
@McpToolParam(description = "First number", required = true) double x,
352+
@McpToolParam(description = "Second number", required = true) double y) {
353+
return x * y;
354+
}
355+
356+
@McpTool(name = "calculate-area",
357+
description = "Calculate the area of a rectangle",
358+
annotations = @McpTool.McpAnnotations(
359+
title = "Rectangle Area Calculator",
360+
readOnlyHint = true,
361+
destructiveHint = false,
362+
idempotentHint = true
363+
))
364+
public AreaResult calculateRectangleArea(
365+
@McpToolParam(description = "Width of the rectangle", required = true) double width,
366+
@McpToolParam(description = "Height of the rectangle", required = true) double height) {
367+
368+
double area = width * height;
369+
return new AreaResult(area, "square units");
370+
}
371+
372+
@McpTool(name = "process-data", description = "Process data with exchange context")
373+
public String processData(
374+
McpSyncServerExchange exchange,
375+
@McpToolParam(description = "Data to process", required = true) String data) {
376+
377+
exchange.loggingNotification(LoggingMessageNotification.builder()
378+
.level(LoggingLevel.INFO)
379+
.data("Processing data: " + data)
380+
.build());
381+
382+
return "Processed: " + data.toUpperCase();
383+
}
384+
385+
// Async tool example
386+
@McpTool(name = "async-calculation", description = "Perform async calculation")
387+
public Mono<String> asyncCalculation(
388+
@McpToolParam(description = "Input value", required = true) int value) {
389+
return Mono.fromCallable(() -> {
390+
// Simulate some async work
391+
Thread.sleep(100);
392+
return "Async result: " + (value * 2);
393+
}).subscribeOn(Schedulers.boundedElastic());
394+
}
395+
396+
public static class AreaResult {
397+
public double area;
398+
public String unit;
399+
400+
public AreaResult(double area, String unit) {
401+
this.area = area;
402+
this.unit = unit;
403+
}
404+
}
405+
}
406+
```
407+
408+
### Async Tool Example
409+
410+
```java
411+
public class AsyncToolProvider {
412+
413+
@McpTool(name = "fetch-data", description = "Fetch data asynchronously")
414+
public Mono<DataResponse> fetchData(
415+
@McpToolParam(description = "Data ID to fetch", required = true) String dataId,
416+
@McpToolParam(description = "Include metadata", required = false) Boolean includeMetadata) {
417+
418+
return Mono.fromCallable(() -> {
419+
// Simulate async data fetching
420+
DataResponse response = new DataResponse();
421+
response.id = dataId;
422+
response.data = "Sample data for " + dataId;
423+
response.metadata = Boolean.TRUE.equals(includeMetadata) ?
424+
Map.of("timestamp", System.currentTimeMillis()) : null;
425+
return response;
426+
}).subscribeOn(Schedulers.boundedElastic());
427+
}
428+
429+
@McpTool(name = "stream-process", description = "Process data stream")
430+
public Flux<String> streamProcess(
431+
@McpToolParam(description = "Number of items to process", required = true) int count) {
432+
433+
return Flux.range(1, count)
434+
.map(i -> "Processed item " + i)
435+
.delayElements(Duration.ofMillis(100));
436+
}
437+
438+
public static class DataResponse {
439+
public String id;
440+
public String data;
441+
public Map<String, Object> metadata;
442+
}
443+
}
444+
```
445+
446+
### Mcp Server with Tool capabilities
447+
448+
```java
449+
public class McpServerFactory {
450+
451+
public McpSyncServer createMcpServerWithTools(
452+
CalculatorToolProvider calculatorProvider,
453+
MyResourceProvider resourceProvider) {
454+
455+
List<SyncToolSpecification> toolSpecifications =
456+
new SyncMcpToolProvider(List.of(calculatorProvider)).getToolSpecifications();
457+
458+
List<SyncResourceSpecification> resourceSpecifications =
459+
new SyncMcpResourceProvider(List.of(resourceProvider)).getResourceSpecifications();
460+
461+
// Create a server with tool support
462+
McpSyncServer syncServer = McpServer.sync(transportProvider)
463+
.serverInfo("calculator-server", "1.0.0")
464+
.capabilities(ServerCapabilities.builder()
465+
.tools(true) // Enable tool support
466+
.resources(true) // Enable resource support
467+
.logging() // Enable logging support
468+
.build())
469+
.tools(toolSpecifications)
470+
.resources(resourceSpecifications)
471+
.build();
472+
473+
return syncServer;
474+
}
475+
476+
public McpAsyncServer createAsyncMcpServerWithTools(
477+
AsyncToolProvider asyncToolProvider) {
478+
479+
List<AsyncToolSpecification> asyncToolSpecifications =
480+
new AsyncMcpToolProvider(List.of(asyncToolProvider)).getToolSpecifications();
481+
482+
// Create an async server with tool support
483+
McpAsyncServer asyncServer = McpServer.async(transportProvider)
484+
.serverInfo("async-tool-server", "1.0.0")
485+
.capabilities(ServerCapabilities.builder()
486+
.tools(true) // Enable tool support
487+
.logging() // Enable logging support
488+
.build())
489+
.tools(asyncToolSpecifications)
490+
.build();
491+
492+
return asyncServer;
493+
}
494+
}
495+
```
496+
328497
### Mcp Server with Resource, Prompt and Completion capabilities
329498

330499
```java
@@ -506,6 +675,18 @@ public class McpConfig {
506675
return SpringAiMcpAnnotationProvider.createSyncResourceSpecifications(resourceProviders);
507676
}
508677

678+
@Bean
679+
public List<SyncToolSpecification> syncToolSpecifications(
680+
List<CalculatorToolProvider> toolProviders) {
681+
return SpringAiMcpAnnotationProvider.createSyncToolSpecifications(toolProviders);
682+
}
683+
684+
@Bean
685+
public List<AsyncToolSpecification> asyncToolSpecifications(
686+
List<AsyncToolProvider> asyncToolProviders) {
687+
return SpringAiMcpAnnotationProvider.createAsyncToolSpecifications(asyncToolProviders);
688+
}
689+
509690
@Bean
510691
public List<Consumer<LoggingMessageNotification>> syncLoggingConsumers(
511692
List<LoggingHandler> loggingHandlers) {
@@ -533,6 +714,7 @@ public class McpConfig {
533714
- **Builder pattern for callback creation** - Clean and fluent API for creating method callbacks
534715
- **Comprehensive validation** - Ensures method signatures are compatible with MCP operations
535716
- **URI template support** - Powerful URI template handling for resource and completion operations
717+
- **Tool support with automatic JSON schema generation** - Create MCP tools with automatic input/output schema generation from method signatures
536718
- **Logging consumer support** - Handle logging message notifications from MCP servers
537719
- **Sampling support** - Handle sampling requests from MCP servers
538720
- **Spring integration** - Seamless integration with Spring Framework and Spring AI

mcp-annotations-spring/src/main/java/org/springaicommunity/mcp/spring/AsyncMcpAnnotationProvider.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121

2222
import org.springaicommunity.mcp.provider.AsyncMcpLoggingConsumerProvider;
2323
import org.springaicommunity.mcp.provider.AsyncMcpSamplingProvider;
24+
import org.springaicommunity.mcp.provider.AsyncMcpToolProvider;
2425

26+
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
2527
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
2628
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
2729
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
@@ -58,6 +60,19 @@ protected Method[] doGetClassMethods(Object bean) {
5860

5961
}
6062

63+
private static class SpringAiAsyncMcpToolProvider extends AsyncMcpToolProvider {
64+
65+
public SpringAiAsyncMcpToolProvider(List<Object> toolObjects) {
66+
super(toolObjects);
67+
}
68+
69+
@Override
70+
protected Method[] doGetClassMethods(Object bean) {
71+
return AnnotationProviderUtil.beanMethods(bean);
72+
}
73+
74+
}
75+
6176
public static List<Function<LoggingMessageNotification, Mono<Void>>> createAsyncLoggingConsumers(
6277
List<Object> loggingObjects) {
6378
return new SpringAiAsyncMcpLoggingConsumerProvider(loggingObjects).getLoggingConsumers();
@@ -68,4 +83,8 @@ public static Function<CreateMessageRequest, Mono<CreateMessageResult>> createAs
6883
return new SpringAiAsyncMcpSamplingProvider(samplingObjects).getSamplingHandler();
6984
}
7085

86+
public static List<AsyncToolSpecification> createAsyncToolSpecifications(List<Object> toolObjects) {
87+
return new SpringAiAsyncMcpToolProvider(toolObjects).getToolSpecifications();
88+
}
89+
7190
}

mcp-annotations-spring/src/main/java/org/springaicommunity/mcp/spring/SyncMcpAnnotationProvider.java

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,20 @@
2020
import java.util.function.Consumer;
2121
import java.util.function.Function;
2222

23-
import org.springaicommunity.mcp.provider.AsyncMcpSamplingProvider;
2423
import org.springaicommunity.mcp.provider.SyncMcpCompletionProvider;
2524
import org.springaicommunity.mcp.provider.SyncMcpLoggingConsumerProvider;
2625
import org.springaicommunity.mcp.provider.SyncMcpPromptProvider;
2726
import org.springaicommunity.mcp.provider.SyncMcpResourceProvider;
2827
import org.springaicommunity.mcp.provider.SyncMcpSamplingProvider;
28+
import org.springaicommunity.mcp.provider.SyncMcpToolProvider;
2929

3030
import io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification;
3131
import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification;
3232
import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification;
33+
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
3334
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
3435
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
3536
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
36-
import reactor.core.publisher.Mono;
3737

3838
/**
3939
* @author Christian Tzolov
@@ -53,6 +53,19 @@ protected Method[] doGetClassMethods(Object bean) {
5353

5454
};
5555

56+
private static class SpringAiSyncToolProvider extends SyncMcpToolProvider {
57+
58+
public SpringAiSyncToolProvider(List<Object> toolObjects) {
59+
super(toolObjects);
60+
}
61+
62+
@Override
63+
protected Method[] doGetClassMethods(Object bean) {
64+
return AnnotationProviderUtil.beanMethods(bean);
65+
}
66+
67+
}
68+
5669
private static class SpringAiSyncMcpPromptProvider extends SyncMcpPromptProvider {
5770

5871
public SpringAiSyncMcpPromptProvider(List<Object> promptObjects) {
@@ -105,17 +118,8 @@ protected Method[] doGetClassMethods(Object bean) {
105118

106119
}
107120

108-
private static class SpringAiAsyncMcpSamplingProvider extends AsyncMcpSamplingProvider {
109-
110-
public SpringAiAsyncMcpSamplingProvider(List<Object> samplingObjects) {
111-
super(samplingObjects);
112-
}
113-
114-
@Override
115-
protected Method[] doGetClassMethods(Object bean) {
116-
return AnnotationProviderUtil.beanMethods(bean);
117-
}
118-
121+
public static List<SyncToolSpecification> createSyncToolSpecifications(List<Object> toolObjects) {
122+
return new SpringAiSyncToolProvider(toolObjects).getToolSpecifications();
119123
}
120124

121125
public static List<SyncCompletionSpecification> createSyncCompleteSpecifications(List<Object> completeObjects) {
@@ -139,9 +143,4 @@ public static Function<CreateMessageRequest, CreateMessageResult> createSyncSamp
139143
return new SpringAiSyncMcpSamplingProvider(samplingObjects).getSamplingHandler();
140144
}
141145

142-
public static Function<CreateMessageRequest, Mono<CreateMessageResult>> createAsyncSamplingHandler(
143-
List<Object> samplingObjects) {
144-
return new SpringAiAsyncMcpSamplingProvider(samplingObjects).getSamplingHandler();
145-
}
146-
147146
}

0 commit comments

Comments
 (0)