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
111 changes: 111 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ The Spring integration module provides seamless integration with Spring AI and S
- **`@McpTool`** - Annotates methods that implement MCP tools with automatic JSON schema generation
- **`@McpToolParam`** - Annotates tool method parameters with descriptions and requirement specifications

#### Special Parameter Annotations
- **`@McpProgressToken`** - Marks a method parameter to receive the progress token from the request. This parameter is automatically injected and excluded from the generated JSON schema

### Method Callbacks

The modules provide callback implementations for each operation type:
Expand Down Expand Up @@ -527,6 +530,114 @@ This feature works with all tool callback types:
- `SyncStatelessMcpToolMethodCallback` - Synchronous stateless
- `AsyncStatelessMcpToolMethodCallback` - Asynchronous stateless

#### @McpProgressToken Support

The `@McpProgressToken` annotation allows methods to receive progress tokens from MCP requests. This is useful for tracking long-running operations and providing progress updates to clients.

When a method parameter is annotated with `@McpProgressToken`:
- The parameter automatically receives the progress token value from the request
- The parameter is excluded from the generated JSON schema
- The parameter type should be `String` to receive the token value
- If no progress token is present in the request, `null` is injected

Example usage with tools:

```java
@McpTool(name = "long-running-task", description = "Performs a long-running task with progress tracking")
public String performLongTask(
@McpProgressToken String progressToken,
@McpToolParam(description = "Task name", required = true) String taskName,
@McpToolParam(description = "Duration in seconds", required = true) int duration) {

// Use the progress token to send progress updates
if (progressToken != null) {
// Send progress notifications using the token
sendProgressUpdate(progressToken, 0.0, "Starting task: " + taskName);

// Simulate work with progress updates
for (int i = 1; i <= duration; i++) {
Thread.sleep(1000);
double progress = (double) i / duration;
sendProgressUpdate(progressToken, progress, "Processing... " + (i * 100 / duration) + "%");
}
}

return "Task " + taskName + " completed successfully";
}

// Tool with both CallToolRequest and progress token
@McpTool(name = "flexible-task", description = "Flexible task with progress tracking")
public CallToolResult flexibleTask(
@McpProgressToken String progressToken,
CallToolRequest request) {

// Access progress token for tracking
if (progressToken != null) {
// Track progress for this operation
System.out.println("Progress token: " + progressToken);
}

// Process the request
Map<String, Object> args = request.arguments();
return CallToolResult.success("Processed with token: " + progressToken);
}
```

The `@McpProgressToken` annotation is also supported in other MCP callback types:

**Resource callbacks:**
```java
@McpResource(uri = "data://{id}", name = "Data Resource", description = "Resource with progress tracking")
public ReadResourceResult getDataWithProgress(
@McpProgressToken String progressToken,
String id) {

if (progressToken != null) {
// Use progress token for tracking resource access
trackResourceAccess(progressToken, id);
}

return new ReadResourceResult(List.of(
new TextResourceContents("data://" + id, "text/plain", "Data for " + id)
));
}
```

**Prompt callbacks:**
```java
@McpPrompt(name = "generate-content", description = "Generate content with progress tracking")
public GetPromptResult generateContent(
@McpProgressToken String progressToken,
@McpArg(name = "topic", required = true) String topic) {

if (progressToken != null) {
// Track prompt generation progress
System.out.println("Generating prompt with token: " + progressToken);
}

return new GetPromptResult("Generated Content",
List.of(new PromptMessage(Role.ASSISTANT, new TextContent("Content about " + topic))));
}
```

**Complete callbacks:**
```java
@McpComplete(prompt = "auto-complete")
public List<String> completeWithProgress(
@McpProgressToken String progressToken,
String prefix) {

if (progressToken != null) {
// Track completion progress
System.out.println("Completion with token: " + progressToken);
}

return generateCompletions(prefix);
}
```

This feature enables better tracking and monitoring of MCP operations, especially for long-running tasks that need to report progress back to clients.

### Async Tool Example

```java
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,19 @@
*
* <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 ProgressMessageNotification}
* MCP servers. The methods takes a single parameter of type {@code ProgressNotification}
*
*
* <p>
* Example usage: <pre>{@code
* &#64;McpProgress
* public void handleProgressMessage(ProgressMessageNotification notification) {
* public void handleProgressMessage(ProgressNotification notification) {
* // Handle the notification *
* }</pre>
*
* @author Christian Tzolov
*
* @see io.modelcontextprotocol.spec.McpSchema.ProgressMessageNotification
* @see io.modelcontextprotocol.spec.McpSchema.ProgressNotification
*/
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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;

/**
* Used to annotate method parameter that should hold the progress token value as received
* from the requester.
*
* @author Christian Tzolov
*/
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface McpProgressToken {

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import org.springaicommunity.mcp.annotation.CompleteAdapter;
import org.springaicommunity.mcp.annotation.McpComplete;
import org.springaicommunity.mcp.annotation.McpProgressToken;

import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CompleteReference;
Expand Down Expand Up @@ -126,20 +127,41 @@ protected void validateMethod(Method method) {
protected void validateParameters(Method method) {
Parameter[] parameters = method.getParameters();

// Check parameter count - must have at most 3 parameters
if (parameters.length > 3) {
throw new IllegalArgumentException("Method can have at most 3 input parameters: " + method.getName()
+ " in " + method.getDeclaringClass().getName() + " has " + parameters.length + " parameters");
// Count non-progress-token parameters
int nonProgressTokenParamCount = 0;
for (Parameter param : parameters) {
if (!param.isAnnotationPresent(McpProgressToken.class)) {
nonProgressTokenParamCount++;
}
}

// Check parameter count - must have at most 3 non-progress-token parameters
if (nonProgressTokenParamCount > 3) {
throw new IllegalArgumentException(
"Method can have at most 3 input parameters (excluding @McpProgressToken): " + method.getName()
+ " in " + method.getDeclaringClass().getName() + " has " + nonProgressTokenParamCount
+ " parameters");
}

// Check parameter types
boolean hasExchangeParam = false;
boolean hasRequestParam = false;
boolean hasArgumentParam = false;
boolean hasProgressTokenParam = false;

for (Parameter param : parameters) {
Class<?> paramType = param.getType();

// Skip @McpProgressToken annotated parameters from validation
if (param.isAnnotationPresent(McpProgressToken.class)) {
if (hasProgressTokenParam) {
throw new IllegalArgumentException("Method cannot have more than one @McpProgressToken parameter: "
+ method.getName() + " in " + method.getDeclaringClass().getName());
}
hasProgressTokenParam = true;
continue;
}

if (isExchangeType(paramType)) {
if (hasExchangeParam) {
throw new IllegalArgumentException("Method cannot have more than one exchange parameter: "
Expand Down Expand Up @@ -184,7 +206,22 @@ protected Object[] buildArgs(Method method, Object exchange, CompleteRequest req
Parameter[] parameters = method.getParameters();
Object[] args = new Object[parameters.length];

// First, handle @McpProgressToken annotated parameters
for (int i = 0; i < parameters.length; i++) {
if (parameters[i].isAnnotationPresent(McpProgressToken.class)) {
// CompleteRequest doesn't have a progressToken method in the current spec
// Set to null for now - this would need to be updated when the spec
// supports it
args[i] = null;
}
}

for (int i = 0; i < parameters.length; i++) {
// Skip if already set (e.g., @McpProgressToken)
if (args[i] != null || parameters[i].isAnnotationPresent(McpProgressToken.class)) {
continue;
}

Parameter param = parameters[i];
Class<?> paramType = param.getType();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.util.Map;

import org.springaicommunity.mcp.annotation.McpArg;
import org.springaicommunity.mcp.annotation.McpProgressToken;

import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
Expand Down Expand Up @@ -87,10 +88,21 @@ protected void validateParameters(Method method) {
boolean hasExchangeParam = false;
boolean hasRequestParam = false;
boolean hasMapParam = false;
boolean hasProgressTokenParam = false;

for (java.lang.reflect.Parameter param : parameters) {
Class<?> paramType = param.getType();

// Skip @McpProgressToken annotated parameters from validation
if (param.isAnnotationPresent(McpProgressToken.class)) {
if (hasProgressTokenParam) {
throw new IllegalArgumentException("Method cannot have more than one @McpProgressToken parameter: "
+ method.getName() + " in " + method.getDeclaringClass().getName());
}
hasProgressTokenParam = true;
continue;
}

if (isExchangeOrContextType(paramType)) {
if (hasExchangeParam) {
throw new IllegalArgumentException("Method cannot have more than one exchange parameter: "
Expand Down Expand Up @@ -130,7 +142,23 @@ protected Object[] buildArgs(Method method, Object exchange, GetPromptRequest re
java.lang.reflect.Parameter[] parameters = method.getParameters();
Object[] args = new Object[parameters.length];

// First, handle @McpProgressToken annotated parameters
for (int i = 0; i < parameters.length; i++) {
if (parameters[i].isAnnotationPresent(McpProgressToken.class)) {
// GetPromptRequest doesn't have a progressToken method in the current
// spec
// Set to null for now - this would need to be updated when the spec
// supports it
args[i] = null;
}
}

for (int i = 0; i < parameters.length; i++) {
// Skip if already set (e.g., @McpProgressToken)
if (args[i] != null || parameters[i].isAnnotationPresent(McpProgressToken.class)) {
continue;
}

java.lang.reflect.Parameter param = parameters[i];
Class<?> paramType = param.getType();

Expand Down
Loading