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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.Content;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -80,24 +81,40 @@ protected Mono<CallToolResult> convertToCallToolResult(Object result) {
.build()));
}

// Handle Flux by taking the first element
// Handle Flux by collecting all elements
if (result instanceof Flux) {
Flux<?> fluxResult = (Flux<?>) result;

// Check if the Flux contains CallToolResult
// Check if the Flux contains CallToolResult — merge all content items
if (ReactiveUtils.isReactiveReturnTypeOfCallToolResult(this.toolMethod)) {
return ((Flux<CallToolResult>) fluxResult).next();
return ((Flux<CallToolResult>) fluxResult).collectList().map(results -> {
var builder = CallToolResult.builder();
for (CallToolResult r : results) {
for (Content c : r.content()) {
builder.addContent(c);
}
}
return builder.build();
});
}

// Handle Mono<Void> for VOID return type
// Handle Flux<Void> for VOID return type
if (ReactiveUtils.isReactiveReturnTypeOfVoid(this.toolMethod)) {
return fluxResult
.then(Mono.just(CallToolResult.builder().addTextContent(JsonParser.toJson("Done")).build()));
}

// Handle other Flux types by taking the first element and mapping
return fluxResult.next()
.map(this::mapValueToCallToolResult)
// Handle other Flux types by collecting all elements
return fluxResult.collectList().map(items -> {
var builder = CallToolResult.builder();
for (Object item : items) {
CallToolResult itemResult = this.mapValueToCallToolResult(item);
for (Content c : itemResult.content()) {
builder.addContent(c);
}
}
return builder.build();
})
.onErrorResume(e -> Mono.just(CallToolResult.builder()
.isError(true)
.addTextContent("Error invoking method: %s".formatted(e.getMessage()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,13 +452,18 @@ public void testMultipleFluxTool() throws Exception {
McpAsyncServerExchange exchange = mock(McpAsyncServerExchange.class);
CallToolRequest request = new CallToolRequest("multiple-flux-tool", Map.of("prefix", "item"));

// Flux tools should take the first element
// Flux tools should collect all elements and return them as separate content
// items
StepVerifier.create(callback.apply(exchange, request)).assertNext(result -> {
assertThat(result).isNotNull();
assertThat(result.isError()).isFalse();
assertThat(result.content()).hasSize(1);
assertThat(result.content()).hasSize(3);
assertThat(result.content().get(0)).isInstanceOf(TextContent.class);
assertThat(((TextContent) result.content().get(0)).text()).isEqualTo("item1");
assertThat(result.content().get(1)).isInstanceOf(TextContent.class);
assertThat(((TextContent) result.content().get(1)).text()).isEqualTo("item2");
assertThat(result.content().get(2)).isInstanceOf(TextContent.class);
assertThat(((TextContent) result.content().get(2)).text()).isEqualTo("item3");
}).verifyComplete();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -473,13 +473,18 @@ public void testMultipleFluxTool() throws Exception {
McpTransportContext context = mock(McpTransportContext.class);
CallToolRequest request = new CallToolRequest("multiple-flux-tool", Map.of("prefix", "item"));

// Flux tools should take the first element
// Flux tools should collect all elements and return them as separate content
// items
StepVerifier.create(callback.apply(context, request)).assertNext(result -> {
assertThat(result).isNotNull();
assertThat(result.isError()).isFalse();
assertThat(result.content()).hasSize(1);
assertThat(result.content()).hasSize(3);
assertThat(result.content().get(0)).isInstanceOf(TextContent.class);
assertThat(((TextContent) result.content().get(0)).text()).isEqualTo("item1");
assertThat(result.content().get(1)).isInstanceOf(TextContent.class);
assertThat(((TextContent) result.content().get(1)).text()).isEqualTo("item2");
assertThat(result.content().get(2)).isInstanceOf(TextContent.class);
assertThat(((TextContent) result.content().get(2)).text()).isEqualTo("item3");
}).verifyComplete();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -533,11 +533,13 @@ public Flux<String> fluxHandlingTool(String input) {
StepVerifier.create(result).assertNext(callToolResult -> {
assertThat(callToolResult).isNotNull();
assertThat(callToolResult.isError()).isFalse();
assertThat(callToolResult.content()).hasSize(1);
assertThat(callToolResult.content()).hasSize(3);
assertThat(callToolResult.content().get(0)).isInstanceOf(TextContent.class);
// Flux results are typically concatenated or collected into a single response
String content = ((TextContent) callToolResult.content().get(0)).text();
assertThat(content).contains("test");
assertThat(((TextContent) callToolResult.content().get(0)).text()).isEqualTo("Item1: test");
assertThat(callToolResult.content().get(1)).isInstanceOf(TextContent.class);
assertThat(((TextContent) callToolResult.content().get(1)).text()).isEqualTo("Item2: test");
assertThat(callToolResult.content().get(2)).isInstanceOf(TextContent.class);
assertThat(((TextContent) callToolResult.content().get(2)).text()).isEqualTo("Item3: test");
}).verifyComplete();
}

Expand Down Expand Up @@ -956,18 +958,13 @@ public Flux<CustomResult> listResponseTool(String input) {

assertThat(result).isNotNull();
assertThat(result.isError()).isFalse();
assertThat(result.content()).hasSize(1);
assertThat(result.content()).hasSize(3);
assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class);

String jsonText = ((TextContent) result.content().get(0)).text();
System.out.println("Actual JSON output: " + jsonText);

// The Flux might be serialized differently than expected, let's check what we
// actually get
// Based on the error, it seems like we're getting a single object instead of an
// array
// Let's adjust our assertion to match the actual behavior
assertThat(jsonText).contains("Processed: test - Item 1");
assertThat(((TextContent) result.content().get(0)).text()).contains("Processed: test - Item 1");
assertThat(result.content().get(1)).isInstanceOf(McpSchema.TextContent.class);
assertThat(((TextContent) result.content().get(1)).text()).contains("Processed: test - Item 2");
assertThat(result.content().get(2)).isInstanceOf(McpSchema.TextContent.class);
assertThat(((TextContent) result.content().get(2)).text()).contains("Processed: test - Item 3");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -529,11 +529,13 @@ public Flux<String> fluxHandlingTool(String input) {
StepVerifier.create(result).assertNext(callToolResult -> {
assertThat(callToolResult).isNotNull();
assertThat(callToolResult.isError()).isFalse();
assertThat(callToolResult.content()).hasSize(1);
assertThat(callToolResult.content()).hasSize(3);
assertThat(callToolResult.content().get(0)).isInstanceOf(TextContent.class);
// Flux results are typically concatenated or collected into a single response
String content = ((TextContent) callToolResult.content().get(0)).text();
assertThat(content).contains("test");
assertThat(((TextContent) callToolResult.content().get(0)).text()).isEqualTo("Item1: test");
assertThat(callToolResult.content().get(1)).isInstanceOf(TextContent.class);
assertThat(((TextContent) callToolResult.content().get(1)).text()).isEqualTo("Item2: test");
assertThat(callToolResult.content().get(2)).isInstanceOf(TextContent.class);
assertThat(((TextContent) callToolResult.content().get(2)).text()).isEqualTo("Item3: test");
}).verifyComplete();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,10 @@ public static class ToolChoiceBuilder {
* Model will not call a function and instead generates a message
*/
public static final String NONE = "none";
/**
* Model must call at least one tool, but it can choose which one.
*/
public static final String REQUIRED = "required";

/**
* Specifying a particular function forces the model to call that function.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,10 @@ public static class ToolChoiceBuilder {
* Model will not call a function and instead generates a message
*/
public static final String NONE = "none";
/**
* Model must call at least one tool, but it can choose which one.
*/
public static final String REQUIRED = "required";

/**
* Specifying a particular function forces the model to call that function.
Expand Down
Loading