feat(mcp): add support for custom MIME types in tool responses
Add capability to specify MIME types for MCP tool responses, with special handling for image content. This enhancement allows tools to return different content types, particularly images, by: - Adding a new toolResponseMimeType map property to configure response MIME types per tool - Extending tool registration methods to accept and use MIME type information - Adding special handling for image content in tool responses - Updating documentation with the new configuration options Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
committed by
Christian Tzolov
parent
dcdfd7a72a
commit
53bb6a5b00
@@ -54,6 +54,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for the Model Context Protocol (MCP)
|
||||
@@ -127,9 +128,21 @@ public class McpServerAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
|
||||
matchIfMissing = true)
|
||||
public List<McpServerFeatures.SyncToolRegistration> syncTools(ObjectProvider<List<ToolCallback>> toolCalls) {
|
||||
var tools = toolCalls.stream().flatMap(List::stream).toList();
|
||||
return McpToolUtils.toSyncToolRegistration(tools);
|
||||
public List<McpServerFeatures.SyncToolRegistration> syncTools(ObjectProvider<List<ToolCallback>> toolCalls,
|
||||
McpServerProperties serverProperties) {
|
||||
List<ToolCallback> tools = toolCalls.stream().flatMap(List::stream).toList();
|
||||
|
||||
return this.toSyncToolRegistration(tools, serverProperties);
|
||||
}
|
||||
|
||||
private List<McpServerFeatures.SyncToolRegistration> toSyncToolRegistration(List<ToolCallback> tools,
|
||||
McpServerProperties serverProperties) {
|
||||
return tools.stream().map(tool -> {
|
||||
String toolName = tool.getToolDefinition().name();
|
||||
MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName))
|
||||
? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null;
|
||||
return McpToolUtils.toSyncToolRegistration(tool, mimeType);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -149,13 +162,16 @@ public class McpServerAutoConfiguration {
|
||||
SyncSpec serverBuilder = McpServer.sync(transport).serverInfo(serverInfo);
|
||||
|
||||
List<SyncToolRegistration> toolResgistrations = new ArrayList<>(tools.stream().flatMap(List::stream).toList());
|
||||
|
||||
List<ToolCallback> providerToolCallbacks = toolCallbackProvider.stream()
|
||||
.map(pr -> List.of(pr.getToolCallbacks()))
|
||||
.flatMap(List::stream)
|
||||
.filter(fc -> fc instanceof ToolCallback)
|
||||
.map(fc -> (ToolCallback) fc)
|
||||
.toList();
|
||||
toolResgistrations.addAll(McpToolUtils.toSyncToolRegistration(providerToolCallbacks));
|
||||
|
||||
toolResgistrations.addAll(this.toSyncToolRegistration(providerToolCallbacks, serverProperties));
|
||||
|
||||
if (!CollectionUtils.isEmpty(toolResgistrations)) {
|
||||
serverBuilder.tools(toolResgistrations);
|
||||
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());
|
||||
@@ -191,9 +207,21 @@ public class McpServerAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
|
||||
public List<McpServerFeatures.AsyncToolRegistration> asyncTools(ObjectProvider<List<ToolCallback>> toolCalls) {
|
||||
public List<McpServerFeatures.AsyncToolRegistration> asyncTools(ObjectProvider<List<ToolCallback>> toolCalls,
|
||||
McpServerProperties serverProperties) {
|
||||
var tools = toolCalls.stream().flatMap(List::stream).toList();
|
||||
return McpToolUtils.toAsyncToolRegistration(tools);
|
||||
|
||||
return this.toAsyncToolRegistration(tools, serverProperties);
|
||||
}
|
||||
|
||||
private List<McpServerFeatures.AsyncToolRegistration> toAsyncToolRegistration(List<ToolCallback> tools,
|
||||
McpServerProperties serverProperties) {
|
||||
return tools.stream().map(tool -> {
|
||||
String toolName = tool.getToolDefinition().name();
|
||||
MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName))
|
||||
? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null;
|
||||
return McpToolUtils.toAsyncToolRegistration(tool, mimeType);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -219,7 +247,9 @@ public class McpServerAutoConfiguration {
|
||||
.filter(fc -> fc instanceof ToolCallback)
|
||||
.map(fc -> (ToolCallback) fc)
|
||||
.toList();
|
||||
toolResgistrations.addAll(McpToolUtils.toAsyncToolRegistration(providerToolCallbacks));
|
||||
|
||||
toolResgistrations.addAll(this.toAsyncToolRegistration(providerToolCallbacks, serverProperties));
|
||||
|
||||
if (!CollectionUtils.isEmpty(toolResgistrations)) {
|
||||
serverBilder.tools(toolResgistrations);
|
||||
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.ai.autoconfigure.mcp.server;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -130,6 +133,11 @@ public class McpServerProperties {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* (Optinal) response MIME type per tool name.
|
||||
*/
|
||||
private Map<String, String> toolResponseMimeType = new HashMap<>();
|
||||
|
||||
public boolean isStdio() {
|
||||
return this.stdio;
|
||||
}
|
||||
@@ -206,4 +214,8 @@ public class McpServerProperties {
|
||||
this.type = serverType;
|
||||
}
|
||||
|
||||
public Map<String, String> getToolResponseMimeType() {
|
||||
return this.toolResponseMimeType;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,12 +22,14 @@ import io.modelcontextprotocol.client.McpSyncClient;
|
||||
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpSchema.Role;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
/**
|
||||
* Utility class that provides helper methods for working with Model Context Protocol
|
||||
@@ -105,12 +107,44 @@ public final class McpToolUtils {
|
||||
* @throws RuntimeException if there's an error during the function execution
|
||||
*/
|
||||
public static McpServerFeatures.SyncToolRegistration toSyncToolRegistration(ToolCallback toolCallback) {
|
||||
return toSyncToolRegistration(toolCallback, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Spring AI FunctionCallback to an MCP SyncToolRegistration. This enables
|
||||
* Spring AI functions to be exposed as MCP tools that can be discovered and invoked
|
||||
* by language models.
|
||||
*
|
||||
* <p>
|
||||
* The conversion process:
|
||||
* <ul>
|
||||
* <li>Creates an MCP Tool with the function's name and input schema</li>
|
||||
* <li>Wraps the function's execution in a SyncToolRegistration that handles the MCP
|
||||
* protocol</li>
|
||||
* <li>Provides error handling and result formatting according to MCP
|
||||
* specifications</li>
|
||||
* </ul>
|
||||
*
|
||||
* You can use the FunctionCallback builder to create a new instance of
|
||||
* FunctionCallback using either java.util.function.Function or Method reference.
|
||||
* @param toolCallback the Spring AI function callback to convert
|
||||
* @param mimeType the MIME type of the output content
|
||||
* @return an MCP SyncToolRegistration that wraps the function callback
|
||||
* @throws RuntimeException if there's an error during the function execution
|
||||
*/
|
||||
public static McpServerFeatures.SyncToolRegistration toSyncToolRegistration(ToolCallback toolCallback,
|
||||
MimeType mimeType) {
|
||||
|
||||
var tool = new McpSchema.Tool(toolCallback.getToolDefinition().name(),
|
||||
toolCallback.getToolDefinition().description(), toolCallback.getToolDefinition().inputSchema());
|
||||
|
||||
return new McpServerFeatures.SyncToolRegistration(tool, request -> {
|
||||
try {
|
||||
String callResult = toolCallback.call(ModelOptionsUtils.toJsonString(request));
|
||||
if (mimeType != null && mimeType.toString().startsWith("image")) {
|
||||
return new McpSchema.CallToolResult(List.of(new McpSchema.ImageContent(List.of(Role.ASSISTANT),
|
||||
null, "image", callResult, mimeType.toString())), false);
|
||||
}
|
||||
return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(callResult)), false);
|
||||
}
|
||||
catch (Exception e) {
|
||||
@@ -174,8 +208,39 @@ public final class McpToolUtils {
|
||||
* @see Schedulers#boundedElastic()
|
||||
*/
|
||||
public static McpServerFeatures.AsyncToolRegistration toAsyncToolRegistration(ToolCallback toolCallback) {
|
||||
return toAsyncToolRegistration(toolCallback, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Spring AI tool callback to an MCP asynchronous tool registration.
|
||||
* <p>
|
||||
* This method enables Spring AI tools to be exposed as asynchronous MCP tools that
|
||||
* can be discovered and invoked by language models. The conversion process:
|
||||
* <ul>
|
||||
* <li>First converts the callback to a synchronous registration</li>
|
||||
* <li>Wraps the synchronous execution in a reactive Mono</li>
|
||||
* <li>Configures execution on a bounded elastic scheduler for non-blocking
|
||||
* operation</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* The resulting async registration will:
|
||||
* <ul>
|
||||
* <li>Execute the tool without blocking the calling thread</li>
|
||||
* <li>Handle errors and results asynchronously</li>
|
||||
* <li>Provide backpressure through Project Reactor</li>
|
||||
* </ul>
|
||||
* @param toolCallback the Spring AI tool callback to convert
|
||||
* @param mimeType the MIME type of the output content
|
||||
* @return an MCP asynchronous tool registration that wraps the tool callback
|
||||
* @see McpServerFeatures.AsyncToolRegistration
|
||||
* @see Mono
|
||||
* @see Schedulers#boundedElastic()
|
||||
*/
|
||||
public static McpServerFeatures.AsyncToolRegistration toAsyncToolRegistration(ToolCallback toolCallback,
|
||||
MimeType mimeType) {
|
||||
|
||||
McpServerFeatures.SyncToolRegistration syncToolRegistration = toSyncToolRegistration(toolCallback, mimeType);
|
||||
|
||||
McpServerFeatures.SyncToolRegistration syncToolRegistration = toSyncToolRegistration(toolCallback);
|
||||
return new AsyncToolRegistration(syncToolRegistration.tool(),
|
||||
map -> Mono.fromCallable(() -> syncToolRegistration.call().apply(map))
|
||||
.subscribeOn(Schedulers.boundedElastic()));
|
||||
|
||||
@@ -87,8 +87,9 @@ All properties are prefixed with `spring.ai.mcp.server`:
|
||||
|`version` |Server version |`1.0.0`
|
||||
|`type` |Server type (SYNC/ASYNC) |`SYNC`
|
||||
|`resource-change-notification` |Enable resource change notifications |`true`
|
||||
|`tool-change-notification` |Enable tool change notifications |`true`
|
||||
|`prompt-change-notification` |Enable prompt change notifications |`true`
|
||||
|`tool-change-notification` |Enable tool change notifications |`true`
|
||||
|`tool-response-mime-type` |(optinal) response MIME type per tool name. For example `spring.ai.mcp.server.tool-response-mime-type.generateImage=image/png` will assosiate the `image/png` mime type with the `generateImage()` tool name |`-`
|
||||
|`sse-message-endpoint` |SSE endpoint path for web transport |`/mcp/message`
|
||||
|===
|
||||
|
||||
|
||||
Reference in New Issue
Block a user