Spring Framework integration for Model Context Protocol (MCP), providing Spring AI function calling capabilities and Spring-friendly abstractions for MCP clients and MCP servers
Convert Spring AI tool context to MCP metadata for passing additional information during tool execution.
import org.springframework.ai.mcp.ToolContextToMcpMetaConverter;
import org.springframework.ai.chat.model.ToolContext;
import java.util.Map;@FunctionalInterface
public interface ToolContextToMcpMetaConverter {
Map<String, Object> convert(ToolContext toolContext);
static ToolContextToMcpMetaConverter defaultConverter();
static ToolContextToMcpMetaConverter noOp();
}Strategy interface for converting a ToolContext to a map of metadata sent as part of an MCP tool call.
static ToolContextToMcpMetaConverter defaultConverter()Returns the default converter implementation that:
McpToolUtils.TOOL_CONTEXT_MCP_EXCHANGE_KEY entryToolContextToMcpMetaConverter defaultConverter =
ToolContextToMcpMetaConverter.defaultConverter();
// Use with tool callback builders
SyncMcpToolCallback callback = SyncMcpToolCallback.builder()
.mcpClient(mcpClient)
.tool(tool)
.toolContextToMcpMetaConverter(defaultConverter)
.build();static ToolContextToMcpMetaConverter noOp()Returns a converter that always returns an empty map, ignoring all tool context.
ToolContextToMcpMetaConverter noOpConverter =
ToolContextToMcpMetaConverter.noOp();
// Use when you don't want to pass any context metadata
AsyncMcpToolCallback callback = AsyncMcpToolCallback.builder()
.mcpClient(asyncClient)
.tool(tool)
.toolContextToMcpMetaConverter(noOpConverter)
.build();// Convert only specific context keys
ToolContextToMcpMetaConverter selectiveConverter = toolContext -> {
if (toolContext == null) return Map.of();
Map<String, Object> context = toolContext.getContext();
Map<String, Object> metadata = new HashMap<>();
// Only pass userId and requestId
if (context.containsKey("userId")) {
metadata.put("userId", context.get("userId"));
}
if (context.containsKey("requestId")) {
metadata.put("requestId", context.get("requestId"));
}
return metadata;
};// Transform values before passing to MCP
ToolContextToMcpMetaConverter transformingConverter = toolContext -> {
if (toolContext == null) return Map.of();
Map<String, Object> metadata = new HashMap<>();
for (Map.Entry<String, Object> entry : toolContext.getContext().entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
// Skip reserved keys
if (key.equals(McpToolUtils.TOOL_CONTEXT_MCP_EXCHANGE_KEY)) {
continue;
}
// Transform timestamp to ISO string
if (value instanceof java.time.Instant) {
metadata.put(key, ((java.time.Instant) value).toString());
}
// Convert complex objects to JSON strings
else if (value != null && !isPrimitive(value)) {
metadata.put(key, ModelOptionsUtils.toJsonString(value));
}
// Pass primitives as-is
else if (value != null) {
metadata.put(key, value);
}
}
return metadata;
};
boolean isPrimitive(Object value) {
return value instanceof String ||
value instanceof Number ||
value instanceof Boolean;
}// Add application-level metadata to all tool calls
ToolContextToMcpMetaConverter enrichingConverter = toolContext -> {
Map<String, Object> metadata = new HashMap<>();
// Add fixed application metadata
metadata.put("appName", "MyApplication");
metadata.put("appVersion", "1.0.0");
metadata.put("environment", System.getenv("ENVIRONMENT"));
// Add context if available
if (toolContext != null && toolContext.getContext() != null) {
for (Map.Entry<String, Object> entry : toolContext.getContext().entrySet()) {
if (!entry.getKey().equals(McpToolUtils.TOOL_CONTEXT_MCP_EXCHANGE_KEY)
&& entry.getValue() != null) {
metadata.put(entry.getKey(), entry.getValue());
}
}
}
return metadata;
};// With SyncMcpToolCallbackProvider
ToolContextToMcpMetaConverter converter = // ... your converter
SyncMcpToolCallbackProvider provider = SyncMcpToolCallbackProvider.builder()
.mcpClients(clients)
.toolContextToMcpMetaConverter(converter)
.build();
// With AsyncMcpToolCallbackProvider
AsyncMcpToolCallbackProvider asyncProvider = AsyncMcpToolCallbackProvider.builder()
.mcpClients(asyncClients)
.toolContextToMcpMetaConverter(converter)
.build();import org.springframework.ai.mcp.*;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.chat.client.ChatClient;
import java.util.Map;
// Define custom converter
ToolContextToMcpMetaConverter customConverter = toolContext -> {
if (toolContext == null) return Map.of();
Map<String, Object> metadata = new HashMap<>();
Map<String, Object> context = toolContext.getContext();
// Pass user information
if (context.containsKey("userId")) {
metadata.put("user_id", context.get("userId"));
}
// Pass request tracing
if (context.containsKey("traceId")) {
metadata.put("trace_id", context.get("traceId"));
}
// Pass permissions
if (context.containsKey("permissions")) {
metadata.put("permissions", context.get("permissions"));
}
// Add timestamp
metadata.put("timestamp", System.currentTimeMillis());
return metadata;
};
// Create provider with custom converter
SyncMcpToolCallbackProvider provider = SyncMcpToolCallbackProvider.builder()
.mcpClients(mcpClient)
.toolContextToMcpMetaConverter(customConverter)
.build();
// Use with Spring AI ChatClient
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultFunctions(provider.getToolCallbacks())
.build();
// Provide context when calling
Map<String, Object> userContext = Map.of(
"userId", "user-123",
"traceId", "trace-456",
"permissions", List.of("read", "write")
);
String response = chatClient.prompt()
.user("List my files")
.toolContext(userContext)
.call()
.content();The converted metadata is passed to the MCP server as part of the CallToolRequest:
// Inside tool callback implementation
var mcpMeta = toolContext != null
? this.toolContextToMcpMetaConverter.convert(toolContext)
: null;
var request = CallToolRequest.builder()
.name(this.tool.name())
.arguments(arguments)
.meta(mcpMeta) // Converted metadata passed here
.build();// Filter sensitive information
ToolContextToMcpMetaConverter secureConverter = toolContext -> {
if (toolContext == null) return Map.of();
Map<String, Object> metadata = new HashMap<>();
List<String> sensitiveKeys = List.of("password", "token", "secret", "apiKey");
for (Map.Entry<String, Object> entry : toolContext.getContext().entrySet()) {
String key = entry.getKey();
// Skip sensitive keys
if (sensitiveKeys.stream().anyMatch(key::toLowerCase()::contains)) {
continue;
}
// Skip reserved keys
if (key.equals(McpToolUtils.TOOL_CONTEXT_MCP_EXCHANGE_KEY)) {
continue;
}
if (entry.getValue() != null) {
metadata.put(key, entry.getValue());
}
}
return metadata;
};