From 50f1b76ce6b3a6e5f29c014664a9886ae495352c 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 9c0c11f8690..616f593e9b6 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.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 McpSchema.ServerCapabilities.Builder capabilitiesBuilder() { @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 McpSyncServer mcpSyncServer(ServerMcpTransport transport, 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 McpSyncServer mcpSyncServer(ServerMcpTransport transport, @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 McpAsyncServer mcpAsyncServer(ServerMcpTransport transport, .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 cc815c4e85a..7df21c3916c 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 enum ServerType { } + /** + * (Optinal) response MIME type per tool name. + */ + private Map toolResponseMimeType = new HashMap<>(); + public boolean isStdio() { return this.stdio; } @@ -206,4 +214,8 @@ public void setType(ServerType serverType) { 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 3a58b9d2737..fe935a5b94c 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.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 static List toSyncToolRegistratio * @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 static List toAsyncToolRegistrat * @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 0f9929a3678..ed0d20359be 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` |===