From 53bb6a5b00dc0cc911460a294e25e835bc562ec5 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Sat, 1 Mar 2025 19:41:19 +0100 Subject: [PATCH] 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 --- .../server/McpServerAutoConfiguration.java | 44 ++++++++++-- .../mcp/server/McpServerProperties.java | 12 ++++ .../springframework/ai/mcp/McpToolUtils.java | 67 ++++++++++++++++++- .../api/mcp/mcp-server-boot-starter-docs.adoc | 3 +- 4 files changed, 117 insertions(+), 9 deletions(-) diff --git a/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfiguration.java b/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfiguration.java index 9c0c11f86..616f593e9 100644 --- a/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfiguration.java +++ b/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfiguration.java @@ -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 syncTools(ObjectProvider> toolCalls) { - var tools = toolCalls.stream().flatMap(List::stream).toList(); - return McpToolUtils.toSyncToolRegistration(tools); + public List syncTools(ObjectProvider> toolCalls, + McpServerProperties serverProperties) { + List tools = toolCalls.stream().flatMap(List::stream).toList(); + + return this.toSyncToolRegistration(tools, serverProperties); + } + + private List toSyncToolRegistration(List 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 toolResgistrations = new ArrayList<>(tools.stream().flatMap(List::stream).toList()); + List 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 asyncTools(ObjectProvider> toolCalls) { + public List asyncTools(ObjectProvider> toolCalls, + McpServerProperties serverProperties) { var tools = toolCalls.stream().flatMap(List::stream).toList(); - return McpToolUtils.toAsyncToolRegistration(tools); + + return this.toAsyncToolRegistration(tools, serverProperties); + } + + private List toAsyncToolRegistration(List 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()); diff --git a/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java b/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java index cc815c4e8..7df21c391 100644 --- a/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java +++ b/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java @@ -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 toolResponseMimeType = new HashMap<>(); + public boolean isStdio() { return this.stdio; } @@ -206,4 +214,8 @@ public class McpServerProperties { this.type = serverType; } + public Map getToolResponseMimeType() { + return this.toolResponseMimeType; + } + } diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java b/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java index 3a58b9d27..fe935a5b9 100644 --- a/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java +++ b/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java @@ -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. + * + *

+ * The conversion process: + *

    + *
  • Creates an MCP Tool with the function's name and input schema
  • + *
  • Wraps the function's execution in a SyncToolRegistration that handles the MCP + * protocol
  • + *
  • Provides error handling and result formatting according to MCP + * specifications
  • + *
+ * + * 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. + *

+ * 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: + *

    + *
  • First converts the callback to a synchronous registration
  • + *
  • Wraps the synchronous execution in a reactive Mono
  • + *
  • Configures execution on a bounded elastic scheduler for non-blocking + * operation
  • + *
+ *

+ * The resulting async registration will: + *

    + *
  • Execute the tool without blocking the calling thread
  • + *
  • Handle errors and results asynchronously
  • + *
  • Provide backpressure through Project Reactor
  • + *
+ * @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())); diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc index 0f9929a36..ed0d20359 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc @@ -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` |===