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` |===