Skip to content

Commit 53bb6a5

Browse files
committed
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 <[email protected]>
1 parent dcdfd7a commit 53bb6a5

File tree

4 files changed

+117
-9
lines changed

4 files changed

+117
-9
lines changed

Diff for: auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfiguration.java

+37-7
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import org.springframework.context.annotation.Bean;
5555
import org.springframework.core.log.LogAccessor;
5656
import org.springframework.util.CollectionUtils;
57+
import org.springframework.util.MimeType;
5758

5859
/**
5960
* {@link EnableAutoConfiguration Auto-configuration} for the Model Context Protocol (MCP)
@@ -127,9 +128,21 @@ public McpSchema.ServerCapabilities.Builder capabilitiesBuilder() {
127128
@Bean
128129
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
129130
matchIfMissing = true)
130-
public List<McpServerFeatures.SyncToolRegistration> syncTools(ObjectProvider<List<ToolCallback>> toolCalls) {
131-
var tools = toolCalls.stream().flatMap(List::stream).toList();
132-
return McpToolUtils.toSyncToolRegistration(tools);
131+
public List<McpServerFeatures.SyncToolRegistration> syncTools(ObjectProvider<List<ToolCallback>> toolCalls,
132+
McpServerProperties serverProperties) {
133+
List<ToolCallback> tools = toolCalls.stream().flatMap(List::stream).toList();
134+
135+
return this.toSyncToolRegistration(tools, serverProperties);
136+
}
137+
138+
private List<McpServerFeatures.SyncToolRegistration> toSyncToolRegistration(List<ToolCallback> tools,
139+
McpServerProperties serverProperties) {
140+
return tools.stream().map(tool -> {
141+
String toolName = tool.getToolDefinition().name();
142+
MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName))
143+
? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null;
144+
return McpToolUtils.toSyncToolRegistration(tool, mimeType);
145+
}).toList();
133146
}
134147

135148
@Bean
@@ -149,13 +162,16 @@ public McpSyncServer mcpSyncServer(ServerMcpTransport transport,
149162
SyncSpec serverBuilder = McpServer.sync(transport).serverInfo(serverInfo);
150163

151164
List<SyncToolRegistration> toolResgistrations = new ArrayList<>(tools.stream().flatMap(List::stream).toList());
165+
152166
List<ToolCallback> providerToolCallbacks = toolCallbackProvider.stream()
153167
.map(pr -> List.of(pr.getToolCallbacks()))
154168
.flatMap(List::stream)
155169
.filter(fc -> fc instanceof ToolCallback)
156170
.map(fc -> (ToolCallback) fc)
157171
.toList();
158-
toolResgistrations.addAll(McpToolUtils.toSyncToolRegistration(providerToolCallbacks));
172+
173+
toolResgistrations.addAll(this.toSyncToolRegistration(providerToolCallbacks, serverProperties));
174+
159175
if (!CollectionUtils.isEmpty(toolResgistrations)) {
160176
serverBuilder.tools(toolResgistrations);
161177
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());
@@ -191,9 +207,21 @@ public McpSyncServer mcpSyncServer(ServerMcpTransport transport,
191207

192208
@Bean
193209
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
194-
public List<McpServerFeatures.AsyncToolRegistration> asyncTools(ObjectProvider<List<ToolCallback>> toolCalls) {
210+
public List<McpServerFeatures.AsyncToolRegistration> asyncTools(ObjectProvider<List<ToolCallback>> toolCalls,
211+
McpServerProperties serverProperties) {
195212
var tools = toolCalls.stream().flatMap(List::stream).toList();
196-
return McpToolUtils.toAsyncToolRegistration(tools);
213+
214+
return this.toAsyncToolRegistration(tools, serverProperties);
215+
}
216+
217+
private List<McpServerFeatures.AsyncToolRegistration> toAsyncToolRegistration(List<ToolCallback> tools,
218+
McpServerProperties serverProperties) {
219+
return tools.stream().map(tool -> {
220+
String toolName = tool.getToolDefinition().name();
221+
MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName))
222+
? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null;
223+
return McpToolUtils.toAsyncToolRegistration(tool, mimeType);
224+
}).toList();
197225
}
198226

199227
@Bean
@@ -219,7 +247,9 @@ public McpAsyncServer mcpAsyncServer(ServerMcpTransport transport,
219247
.filter(fc -> fc instanceof ToolCallback)
220248
.map(fc -> (ToolCallback) fc)
221249
.toList();
222-
toolResgistrations.addAll(McpToolUtils.toAsyncToolRegistration(providerToolCallbacks));
250+
251+
toolResgistrations.addAll(this.toAsyncToolRegistration(providerToolCallbacks, serverProperties));
252+
223253
if (!CollectionUtils.isEmpty(toolResgistrations)) {
224254
serverBilder.tools(toolResgistrations);
225255
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());

Diff for: auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java

+12
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package org.springframework.ai.autoconfigure.mcp.server;
1818

19+
import java.util.HashMap;
20+
import java.util.Map;
21+
1922
import org.springframework.boot.context.properties.ConfigurationProperties;
2023
import org.springframework.util.Assert;
2124

@@ -130,6 +133,11 @@ public enum ServerType {
130133

131134
}
132135

136+
/**
137+
* (Optinal) response MIME type per tool name.
138+
*/
139+
private Map<String, String> toolResponseMimeType = new HashMap<>();
140+
133141
public boolean isStdio() {
134142
return this.stdio;
135143
}
@@ -206,4 +214,8 @@ public void setType(ServerType serverType) {
206214
this.type = serverType;
207215
}
208216

217+
public Map<String, String> getToolResponseMimeType() {
218+
return this.toolResponseMimeType;
219+
}
220+
209221
}

Diff for: mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java

+66-1
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@
2222
import io.modelcontextprotocol.server.McpServerFeatures;
2323
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration;
2424
import io.modelcontextprotocol.spec.McpSchema;
25+
import io.modelcontextprotocol.spec.McpSchema.Role;
2526
import reactor.core.publisher.Mono;
2627
import reactor.core.scheduler.Schedulers;
2728

2829
import org.springframework.ai.model.ModelOptionsUtils;
2930
import org.springframework.ai.tool.ToolCallback;
3031
import org.springframework.util.CollectionUtils;
32+
import org.springframework.util.MimeType;
3133

3234
/**
3335
* Utility class that provides helper methods for working with Model Context Protocol
@@ -105,12 +107,44 @@ public static List<McpServerFeatures.SyncToolRegistration> toSyncToolRegistratio
105107
* @throws RuntimeException if there's an error during the function execution
106108
*/
107109
public static McpServerFeatures.SyncToolRegistration toSyncToolRegistration(ToolCallback toolCallback) {
110+
return toSyncToolRegistration(toolCallback, null);
111+
}
112+
113+
/**
114+
* Converts a Spring AI FunctionCallback to an MCP SyncToolRegistration. This enables
115+
* Spring AI functions to be exposed as MCP tools that can be discovered and invoked
116+
* by language models.
117+
*
118+
* <p>
119+
* The conversion process:
120+
* <ul>
121+
* <li>Creates an MCP Tool with the function's name and input schema</li>
122+
* <li>Wraps the function's execution in a SyncToolRegistration that handles the MCP
123+
* protocol</li>
124+
* <li>Provides error handling and result formatting according to MCP
125+
* specifications</li>
126+
* </ul>
127+
*
128+
* You can use the FunctionCallback builder to create a new instance of
129+
* FunctionCallback using either java.util.function.Function or Method reference.
130+
* @param toolCallback the Spring AI function callback to convert
131+
* @param mimeType the MIME type of the output content
132+
* @return an MCP SyncToolRegistration that wraps the function callback
133+
* @throws RuntimeException if there's an error during the function execution
134+
*/
135+
public static McpServerFeatures.SyncToolRegistration toSyncToolRegistration(ToolCallback toolCallback,
136+
MimeType mimeType) {
137+
108138
var tool = new McpSchema.Tool(toolCallback.getToolDefinition().name(),
109139
toolCallback.getToolDefinition().description(), toolCallback.getToolDefinition().inputSchema());
110140

111141
return new McpServerFeatures.SyncToolRegistration(tool, request -> {
112142
try {
113143
String callResult = toolCallback.call(ModelOptionsUtils.toJsonString(request));
144+
if (mimeType != null && mimeType.toString().startsWith("image")) {
145+
return new McpSchema.CallToolResult(List.of(new McpSchema.ImageContent(List.of(Role.ASSISTANT),
146+
null, "image", callResult, mimeType.toString())), false);
147+
}
114148
return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(callResult)), false);
115149
}
116150
catch (Exception e) {
@@ -174,8 +208,39 @@ public static List<McpServerFeatures.AsyncToolRegistration> toAsyncToolRegistrat
174208
* @see Schedulers#boundedElastic()
175209
*/
176210
public static McpServerFeatures.AsyncToolRegistration toAsyncToolRegistration(ToolCallback toolCallback) {
211+
return toAsyncToolRegistration(toolCallback, null);
212+
}
213+
214+
/**
215+
* Converts a Spring AI tool callback to an MCP asynchronous tool registration.
216+
* <p>
217+
* This method enables Spring AI tools to be exposed as asynchronous MCP tools that
218+
* can be discovered and invoked by language models. The conversion process:
219+
* <ul>
220+
* <li>First converts the callback to a synchronous registration</li>
221+
* <li>Wraps the synchronous execution in a reactive Mono</li>
222+
* <li>Configures execution on a bounded elastic scheduler for non-blocking
223+
* operation</li>
224+
* </ul>
225+
* <p>
226+
* The resulting async registration will:
227+
* <ul>
228+
* <li>Execute the tool without blocking the calling thread</li>
229+
* <li>Handle errors and results asynchronously</li>
230+
* <li>Provide backpressure through Project Reactor</li>
231+
* </ul>
232+
* @param toolCallback the Spring AI tool callback to convert
233+
* @param mimeType the MIME type of the output content
234+
* @return an MCP asynchronous tool registration that wraps the tool callback
235+
* @see McpServerFeatures.AsyncToolRegistration
236+
* @see Mono
237+
* @see Schedulers#boundedElastic()
238+
*/
239+
public static McpServerFeatures.AsyncToolRegistration toAsyncToolRegistration(ToolCallback toolCallback,
240+
MimeType mimeType) {
241+
242+
McpServerFeatures.SyncToolRegistration syncToolRegistration = toSyncToolRegistration(toolCallback, mimeType);
177243

178-
McpServerFeatures.SyncToolRegistration syncToolRegistration = toSyncToolRegistration(toolCallback);
179244
return new AsyncToolRegistration(syncToolRegistration.tool(),
180245
map -> Mono.fromCallable(() -> syncToolRegistration.call().apply(map))
181246
.subscribeOn(Schedulers.boundedElastic()));

Diff for: spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc

+2-1
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ All properties are prefixed with `spring.ai.mcp.server`:
8787
|`version` |Server version |`1.0.0`
8888
|`type` |Server type (SYNC/ASYNC) |`SYNC`
8989
|`resource-change-notification` |Enable resource change notifications |`true`
90-
|`tool-change-notification` |Enable tool change notifications |`true`
9190
|`prompt-change-notification` |Enable prompt change notifications |`true`
91+
|`tool-change-notification` |Enable tool change notifications |`true`
92+
|`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 |`-`
9293
|`sse-message-endpoint` |SSE endpoint path for web transport |`/mcp/message`
9394
|===
9495

0 commit comments

Comments
 (0)