Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -127,9 +128,21 @@ public McpSchema.ServerCapabilities.Builder capabilitiesBuilder() {
@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
Expand All @@ -149,13 +162,16 @@ public McpSyncServer mcpSyncServer(ServerMcpTransport transport,
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());
Expand Down Expand Up @@ -191,9 +207,21 @@ public McpSyncServer mcpSyncServer(ServerMcpTransport transport,

@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
Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -130,6 +133,11 @@ public enum ServerType {

}

/**
* (Optinal) response MIME type per tool name.
*/
private Map<String, String> toolResponseMimeType = new HashMap<>();

public boolean isStdio() {
return this.stdio;
}
Expand Down Expand Up @@ -206,4 +214,8 @@ public void setType(ServerType serverType) {
this.type = serverType;
}

public Map<String, String> getToolResponseMimeType() {
return this.toolResponseMimeType;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -105,12 +107,44 @@ public static List<McpServerFeatures.SyncToolRegistration> 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.
*
* <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) {
Expand Down Expand Up @@ -174,8 +208,39 @@ public static List<McpServerFeatures.AsyncToolRegistration> 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.
* <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()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`
|===

Expand Down