Skip to content

Commit e321cc3

Browse files
committed
Merge remote-tracking branch 'upstream/main' into GH-2294-2298
2 parents 5275cec + 2394ac8 commit e321cc3

File tree

52 files changed

+659
-351
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+659
-351
lines changed

auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseHttpClientTransportAutoConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
*/
6464
@AutoConfiguration(after = SseWebFluxTransportAutoConfiguration.class)
6565
@ConditionalOnClass({ McpSchema.class, McpSyncClient.class })
66-
@ConditionalOnMissingClass("io.modelcontextprotocol.client.transport.public class WebFluxSseClientTransport")
66+
@ConditionalOnMissingClass("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport")
6767
@EnableConfigurationProperties({ McpSseClientProperties.class, McpClientCommonProperties.class })
6868
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
6969
matchIfMissing = true)

auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfiguration.java

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import org.springframework.context.annotation.Bean;
5555
import org.springframework.core.log.LogAccessor;
5656
import org.springframework.util.CollectionUtils;
57+
import org.springframework.util.MimeType;
5758

5859
/**
5960
* {@link EnableAutoConfiguration Auto-configuration} for the Model Context Protocol (MCP)
@@ -127,9 +128,21 @@ public McpSchema.ServerCapabilities.Builder capabilitiesBuilder() {
127128
@Bean
128129
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
129130
matchIfMissing = true)
130-
public List<McpServerFeatures.SyncToolRegistration> syncTools(ObjectProvider<List<ToolCallback>> toolCalls) {
131-
var tools = toolCalls.stream().flatMap(List::stream).toList();
132-
return McpToolUtils.toSyncToolRegistration(tools);
131+
public List<McpServerFeatures.SyncToolRegistration> syncTools(ObjectProvider<List<ToolCallback>> toolCalls,
132+
McpServerProperties serverProperties) {
133+
List<ToolCallback> tools = toolCalls.stream().flatMap(List::stream).toList();
134+
135+
return this.toSyncToolRegistration(tools, serverProperties);
136+
}
137+
138+
private List<McpServerFeatures.SyncToolRegistration> toSyncToolRegistration(List<ToolCallback> tools,
139+
McpServerProperties serverProperties) {
140+
return tools.stream().map(tool -> {
141+
String toolName = tool.getToolDefinition().name();
142+
MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName))
143+
? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null;
144+
return McpToolUtils.toSyncToolRegistration(tool, mimeType);
145+
}).toList();
133146
}
134147

135148
@Bean
@@ -149,13 +162,16 @@ public McpSyncServer mcpSyncServer(ServerMcpTransport transport,
149162
SyncSpec serverBuilder = McpServer.sync(transport).serverInfo(serverInfo);
150163

151164
List<SyncToolRegistration> toolResgistrations = new ArrayList<>(tools.stream().flatMap(List::stream).toList());
165+
152166
List<ToolCallback> providerToolCallbacks = toolCallbackProvider.stream()
153167
.map(pr -> List.of(pr.getToolCallbacks()))
154168
.flatMap(List::stream)
155169
.filter(fc -> fc instanceof ToolCallback)
156170
.map(fc -> (ToolCallback) fc)
157171
.toList();
158-
toolResgistrations.addAll(McpToolUtils.toSyncToolRegistration(providerToolCallbacks));
172+
173+
toolResgistrations.addAll(this.toSyncToolRegistration(providerToolCallbacks, serverProperties));
174+
159175
if (!CollectionUtils.isEmpty(toolResgistrations)) {
160176
serverBuilder.tools(toolResgistrations);
161177
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());
@@ -191,9 +207,21 @@ public McpSyncServer mcpSyncServer(ServerMcpTransport transport,
191207

192208
@Bean
193209
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
194-
public List<McpServerFeatures.AsyncToolRegistration> asyncTools(ObjectProvider<List<ToolCallback>> toolCalls) {
210+
public List<McpServerFeatures.AsyncToolRegistration> asyncTools(ObjectProvider<List<ToolCallback>> toolCalls,
211+
McpServerProperties serverProperties) {
195212
var tools = toolCalls.stream().flatMap(List::stream).toList();
196-
return McpToolUtils.toAsyncToolRegistration(tools);
213+
214+
return this.toAsyncToolRegistration(tools, serverProperties);
215+
}
216+
217+
private List<McpServerFeatures.AsyncToolRegistration> toAsyncToolRegistration(List<ToolCallback> tools,
218+
McpServerProperties serverProperties) {
219+
return tools.stream().map(tool -> {
220+
String toolName = tool.getToolDefinition().name();
221+
MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName))
222+
? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null;
223+
return McpToolUtils.toAsyncToolRegistration(tool, mimeType);
224+
}).toList();
197225
}
198226

199227
@Bean
@@ -219,7 +247,9 @@ public McpAsyncServer mcpAsyncServer(ServerMcpTransport transport,
219247
.filter(fc -> fc instanceof ToolCallback)
220248
.map(fc -> (ToolCallback) fc)
221249
.toList();
222-
toolResgistrations.addAll(McpToolUtils.toAsyncToolRegistration(providerToolCallbacks));
250+
251+
toolResgistrations.addAll(this.toAsyncToolRegistration(providerToolCallbacks, serverProperties));
252+
223253
if (!CollectionUtils.isEmpty(toolResgistrations)) {
224254
serverBilder.tools(toolResgistrations);
225255
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());

auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package org.springframework.ai.autoconfigure.mcp.server;
1818

19+
import java.util.HashMap;
20+
import java.util.Map;
21+
1922
import org.springframework.boot.context.properties.ConfigurationProperties;
2023
import org.springframework.util.Assert;
2124

@@ -130,6 +133,11 @@ public enum ServerType {
130133

131134
}
132135

136+
/**
137+
* (Optinal) response MIME type per tool name.
138+
*/
139+
private Map<String, String> toolResponseMimeType = new HashMap<>();
140+
133141
public boolean isStdio() {
134142
return this.stdio;
135143
}
@@ -206,4 +214,8 @@ public void setType(ServerType serverType) {
206214
this.type = serverType;
207215
}
208216

217+
public Map<String, String> getToolResponseMimeType() {
218+
return this.toolResponseMimeType;
219+
}
220+
209221
}

document-readers/tika-reader/src/test/java/org/springframework/ai/reader/tika/TikaDocumentReaderTests.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@
1818

1919
import org.junit.jupiter.params.ParameterizedTest;
2020
import org.junit.jupiter.params.provider.CsvSource;
21-
21+
import org.springframework.ai.reader.ExtractedTextFormatter;
2222
import static org.assertj.core.api.Assertions.assertThat;
23+
import static org.junit.jupiter.api.Assertions.assertFalse;
24+
import static org.junit.jupiter.api.Assertions.assertTrue;
2325

2426
/**
2527
* @author Christian Tzolov
28+
* @author Shahbaz Aamir
2629
*/
2730
public class TikaDocumentReaderTests {
2831

@@ -46,4 +49,26 @@ public void testDocx(String resourceUri, String resourceName, String contentSnip
4649
assertThat(doc.getText()).contains(contentSnipped);
4750
}
4851

52+
@ParameterizedTest
53+
@CsvSource({
54+
"classpath:/word-sample.docx,word-sample.docx,This document demonstrates the ability of the calibre DOCX Input plugin",
55+
"classpath:/sample2.pdf,sample2.pdf,Robert Maron", "classpath:/sample.ppt,sample.ppt,Sample FILE",
56+
"classpath:/sample.pptx,sample.pptx,Sample FILE" })
57+
public void testReaderWithFormatter(String resourceUri, String resourceName, String contentSnipped) {
58+
59+
ExtractedTextFormatter formatter = ExtractedTextFormatter.builder().withNumberOfTopTextLinesToDelete(5).build();
60+
var docs = new TikaDocumentReader(resourceUri, formatter).get();
61+
62+
assertThat(docs).hasSize(1);
63+
64+
var doc = docs.get(0);
65+
66+
assertThat(doc.getMetadata()).containsKeys(TikaDocumentReader.METADATA_SOURCE);
67+
assertThat(doc.getMetadata().get(TikaDocumentReader.METADATA_SOURCE)).isEqualTo(resourceName);
68+
assertFalse(doc.getText().contains(contentSnipped));
69+
docs = new TikaDocumentReader(resourceUri).get();
70+
doc = docs.get(0);
71+
assertThat(doc.getText()).contains(contentSnipped);
72+
}
73+
4974
}

mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@
2222
import io.modelcontextprotocol.server.McpServerFeatures;
2323
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration;
2424
import io.modelcontextprotocol.spec.McpSchema;
25+
import io.modelcontextprotocol.spec.McpSchema.Role;
2526
import reactor.core.publisher.Mono;
2627
import reactor.core.scheduler.Schedulers;
2728

2829
import org.springframework.ai.model.ModelOptionsUtils;
2930
import org.springframework.ai.tool.ToolCallback;
3031
import org.springframework.util.CollectionUtils;
32+
import org.springframework.util.MimeType;
3133

3234
/**
3335
* Utility class that provides helper methods for working with Model Context Protocol
@@ -105,12 +107,44 @@ public static List<McpServerFeatures.SyncToolRegistration> toSyncToolRegistratio
105107
* @throws RuntimeException if there's an error during the function execution
106108
*/
107109
public static McpServerFeatures.SyncToolRegistration toSyncToolRegistration(ToolCallback toolCallback) {
110+
return toSyncToolRegistration(toolCallback, null);
111+
}
112+
113+
/**
114+
* Converts a Spring AI FunctionCallback to an MCP SyncToolRegistration. This enables
115+
* Spring AI functions to be exposed as MCP tools that can be discovered and invoked
116+
* by language models.
117+
*
118+
* <p>
119+
* The conversion process:
120+
* <ul>
121+
* <li>Creates an MCP Tool with the function's name and input schema</li>
122+
* <li>Wraps the function's execution in a SyncToolRegistration that handles the MCP
123+
* protocol</li>
124+
* <li>Provides error handling and result formatting according to MCP
125+
* specifications</li>
126+
* </ul>
127+
*
128+
* You can use the FunctionCallback builder to create a new instance of
129+
* FunctionCallback using either java.util.function.Function or Method reference.
130+
* @param toolCallback the Spring AI function callback to convert
131+
* @param mimeType the MIME type of the output content
132+
* @return an MCP SyncToolRegistration that wraps the function callback
133+
* @throws RuntimeException if there's an error during the function execution
134+
*/
135+
public static McpServerFeatures.SyncToolRegistration toSyncToolRegistration(ToolCallback toolCallback,
136+
MimeType mimeType) {
137+
108138
var tool = new McpSchema.Tool(toolCallback.getToolDefinition().name(),
109139
toolCallback.getToolDefinition().description(), toolCallback.getToolDefinition().inputSchema());
110140

111141
return new McpServerFeatures.SyncToolRegistration(tool, request -> {
112142
try {
113143
String callResult = toolCallback.call(ModelOptionsUtils.toJsonString(request));
144+
if (mimeType != null && mimeType.toString().startsWith("image")) {
145+
return new McpSchema.CallToolResult(List.of(new McpSchema.ImageContent(List.of(Role.ASSISTANT),
146+
null, "image", callResult, mimeType.toString())), false);
147+
}
114148
return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(callResult)), false);
115149
}
116150
catch (Exception e) {
@@ -174,8 +208,39 @@ public static List<McpServerFeatures.AsyncToolRegistration> toAsyncToolRegistrat
174208
* @see Schedulers#boundedElastic()
175209
*/
176210
public static McpServerFeatures.AsyncToolRegistration toAsyncToolRegistration(ToolCallback toolCallback) {
211+
return toAsyncToolRegistration(toolCallback, null);
212+
}
213+
214+
/**
215+
* Converts a Spring AI tool callback to an MCP asynchronous tool registration.
216+
* <p>
217+
* This method enables Spring AI tools to be exposed as asynchronous MCP tools that
218+
* can be discovered and invoked by language models. The conversion process:
219+
* <ul>
220+
* <li>First converts the callback to a synchronous registration</li>
221+
* <li>Wraps the synchronous execution in a reactive Mono</li>
222+
* <li>Configures execution on a bounded elastic scheduler for non-blocking
223+
* operation</li>
224+
* </ul>
225+
* <p>
226+
* The resulting async registration will:
227+
* <ul>
228+
* <li>Execute the tool without blocking the calling thread</li>
229+
* <li>Handle errors and results asynchronously</li>
230+
* <li>Provide backpressure through Project Reactor</li>
231+
* </ul>
232+
* @param toolCallback the Spring AI tool callback to convert
233+
* @param mimeType the MIME type of the output content
234+
* @return an MCP asynchronous tool registration that wraps the tool callback
235+
* @see McpServerFeatures.AsyncToolRegistration
236+
* @see Mono
237+
* @see Schedulers#boundedElastic()
238+
*/
239+
public static McpServerFeatures.AsyncToolRegistration toAsyncToolRegistration(ToolCallback toolCallback,
240+
MimeType mimeType) {
241+
242+
McpServerFeatures.SyncToolRegistration syncToolRegistration = toSyncToolRegistration(toolCallback, mimeType);
177243

178-
McpServerFeatures.SyncToolRegistration syncToolRegistration = toSyncToolRegistration(toolCallback);
179244
return new AsyncToolRegistration(syncToolRegistration.tool(),
180245
map -> Mono.fromCallable(() -> syncToolRegistration.call().apply(map))
181246
.subscribeOn(Schedulers.boundedElastic()));

models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatModel.java

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.ArrayList;
2020
import java.util.Base64;
21+
import java.util.HashMap;
2122
import java.util.List;
2223
import java.util.Map;
2324
import java.util.Set;
@@ -78,6 +79,7 @@
7879
import org.springframework.retry.support.RetryTemplate;
7980
import org.springframework.util.Assert;
8081
import org.springframework.util.CollectionUtils;
82+
import org.springframework.util.MultiValueMap;
8183
import org.springframework.util.StringUtils;
8284

8385
/**
@@ -93,7 +95,7 @@
9395
*/
9496
public class AnthropicChatModel extends AbstractToolCallSupport implements ChatModel {
9597

96-
public static final String DEFAULT_MODEL_NAME = AnthropicApi.ChatModel.CLAUDE_3_5_SONNET.getValue();
98+
public static final String DEFAULT_MODEL_NAME = AnthropicApi.ChatModel.CLAUDE_3_7_SONNET.getValue();
9799

98100
public static final Integer DEFAULT_MAX_TOKENS = 500;
99101

@@ -273,8 +275,8 @@ public ChatResponse internalCall(Prompt prompt, ChatResponse previousChatRespons
273275
this.observationRegistry)
274276
.observe(() -> {
275277

276-
ResponseEntity<ChatCompletionResponse> completionEntity = this.retryTemplate
277-
.execute(ctx -> this.anthropicApi.chatCompletionEntity(request));
278+
ResponseEntity<ChatCompletionResponse> completionEntity = this.retryTemplate.execute(
279+
ctx -> this.anthropicApi.chatCompletionEntity(request, this.getAdditionalHttpHeaders(prompt)));
278280

279281
AnthropicApi.ChatCompletionResponse completionResponse = completionEntity.getBody();
280282
AnthropicApi.Usage usage = completionResponse.usage();
@@ -338,7 +340,8 @@ public Flux<ChatResponse> internalStream(Prompt prompt, ChatResponse previousCha
338340

339341
observation.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null)).start();
340342

341-
Flux<ChatCompletionResponse> response = this.anthropicApi.chatCompletionStream(request);
343+
Flux<ChatCompletionResponse> response = this.anthropicApi.chatCompletionStream(request,
344+
this.getAdditionalHttpHeaders(prompt));
342345

343346
// @formatter:off
344347
Flux<ChatResponse> chatResponseFlux = response.switchMap(chatCompletionResponse -> {
@@ -462,6 +465,16 @@ else if (mimeType.contains("pdf")) {
462465
+ ". Supported types are: images (image/*) and PDF documents (application/pdf)");
463466
}
464467

468+
private MultiValueMap<String, String> getAdditionalHttpHeaders(Prompt prompt) {
469+
470+
Map<String, String> headers = new HashMap<>(this.defaultOptions.getHttpHeaders());
471+
if (prompt.getOptions() != null && prompt.getOptions() instanceof AnthropicChatOptions chatOptions) {
472+
headers.putAll(chatOptions.getHttpHeaders());
473+
}
474+
return CollectionUtils.toMultiValueMap(
475+
headers.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> List.of(e.getValue()))));
476+
}
477+
465478
Prompt buildRequestPrompt(Prompt prompt) {
466479
// Process runtime options
467480
AnthropicChatOptions runtimeOptions = null;
@@ -487,6 +500,8 @@ else if (prompt.getOptions() instanceof FunctionCallingOptions functionCallingOp
487500
// Merge @JsonIgnore-annotated options explicitly since they are ignored by
488501
// Jackson, used by ModelOptionsUtils.
489502
if (runtimeOptions != null) {
503+
requestOptions.setHttpHeaders(
504+
mergeHttpHeaders(runtimeOptions.getHttpHeaders(), this.defaultOptions.getHttpHeaders()));
490505
requestOptions.setInternalToolExecutionEnabled(
491506
ModelOptionsUtils.mergeOption(runtimeOptions.isInternalToolExecutionEnabled(),
492507
this.defaultOptions.isInternalToolExecutionEnabled()));
@@ -498,8 +513,8 @@ else if (prompt.getOptions() instanceof FunctionCallingOptions functionCallingOp
498513
this.defaultOptions.getToolContext()));
499514
}
500515
else {
516+
requestOptions.setHttpHeaders(this.defaultOptions.getHttpHeaders());
501517
requestOptions.setInternalToolExecutionEnabled(this.defaultOptions.isInternalToolExecutionEnabled());
502-
503518
requestOptions.setToolNames(this.defaultOptions.getToolNames());
504519
requestOptions.setToolCallbacks(this.defaultOptions.getToolCallbacks());
505520
requestOptions.setToolContext(this.defaultOptions.getToolContext());
@@ -510,6 +525,13 @@ else if (prompt.getOptions() instanceof FunctionCallingOptions functionCallingOp
510525
return new Prompt(prompt.getInstructions(), requestOptions);
511526
}
512527

528+
private Map<String, String> mergeHttpHeaders(Map<String, String> runtimeHttpHeaders,
529+
Map<String, String> defaultHttpHeaders) {
530+
var mergedHttpHeaders = new HashMap<>(defaultHttpHeaders);
531+
mergedHttpHeaders.putAll(runtimeHttpHeaders);
532+
return mergedHttpHeaders;
533+
}
534+
513535
ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
514536

515537
List<AnthropicMessage> userMessages = prompt.getInstructions()

0 commit comments

Comments
 (0)