Bridge Java and Python AI: Integrate MCP with Spring AI for Seamless Tool Calls
This article walks through how Java developers can connect to Python‑based AI services using the Model Context Protocol (MCP), compares STDIO and SSE transports, explains why Spring AI’s MCP support is limited, and shows a complete implementation with the raw MCP Java SDK and OpenAI client to invoke tools like Blender from Java code.
Background
Most MCP servers are written in Python, while many enterprise back‑ends use Java. To avoid rewriting MCP servers, Java developers need a way to call these services directly.
MCP Overview
MCP (Model Context Protocol) is a JSON‑RPC 2.0 based, language‑agnostic protocol that defines standard messages for tool discovery, invocation, resource handling and streaming.
Transport Options
STDIO : uses process stdin/stdout, suitable for demos.
SSE : Server‑Sent Events over HTTP, ideal for production and decoupled services.
Spring AI Integration Issues
Spring AI’s spring-ai-starter-mcp-client sends empty POST bodies when using SSE, causing a JSONDecodeError on the Python side. The framework also prints its startup banner to stdout, which interferes with MCP’s STDIO mode.
Work‑arounds such as spring.main.web-application-type=none and spring.main.banner-mode=off are required, but the underlying bug remains.
Raw MCP Java SDK Solution
Instead of Spring AI, the article builds a client with the official MCP Java SDK and the OpenAI Java SDK. The workflow is:
Discover tools from each MCP server via listTools().
Map each tool to an OpenAI FunctionDefinition.
Send an initial ChatCompletion request. If the model returns function_call, invoke the corresponding MCP tool.
Append the tool result to the conversation and repeat until the model replies with plain text.
Key Java Code
package com.alibaba.damo.mcpclient.client;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.FunctionDefinition;
import com.openai.models.FunctionParameters;
import com.openai.models.chat.completions.*;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class MyMCPClient {
private static final Logger logger = LoggerFactory.getLogger(MyMCPClient.class);
@Value("${spring.ai.openai.base-url}") private String baseUrl;
@Value("${spring.ai.openai.api-key}") private String apiKey;
@Value("${spring.ai.openai.chat.options.model}") private String model;
@Value("${mcp.servers}") private String toolServerMapping; // e.g. blender=http://localhost:8000
private OpenAIClient openaiClient;
private final Map<String, McpSyncClient> toolToClient = new HashMap<>();
private final List<McpSchema.Tool> allTools = new ArrayList<>();
@PostConstruct
public void init() {
// Initialise MCP clients for each server
Arrays.stream(toolServerMapping.split(","))
.map(entry -> entry.split("="))
.forEach(pair -> {
String url = pair[1];
McpSyncClient client = McpClient.sync(
HttpClientSseClientTransport.builder(url).build()
).build();
client.initialize();
logger.info("Connected to MCP server via SSE at {}", url);
List<McpSchema.Tool> tools = client.listTools().tools();
allTools.addAll(tools);
tools.forEach(t -> toolToClient.put(t.name(), client));
});
// Initialise OpenAI client
this.openaiClient = OpenAIOkHttpClient.builder()
.baseUrl(baseUrl)
.apiKey(apiKey)
.checkJacksonVersionCompatibility(false)
.build();
logger.info("OpenAI client initialized with model {}", model);
}
@PreDestroy
public void shutdown() {
toolToClient.values().forEach(c -> {
try { c.close(); logger.info("Closed MCP client for {}", c); }
catch (Exception e) { logger.warn("Error closing MCP client: {}", e.getMessage()); }
});
}
public String processQuery(String query) {
try {
// Convert MCP tools to OpenAI function definitions
List<ChatCompletionTool> chatTools = allTools.stream()
.map(t -> ChatCompletionTool.builder()
.function(FunctionDefinition.builder()
.name(t.name())
.description(t.description())
.parameters(FunctionParameters.builder()
.putAdditionalProperty("type", JsonValue.from(t.inputSchema().type()))
.putAdditionalProperty("properties", JsonValue.from(t.inputSchema().properties()))
.putAdditionalProperty("required", JsonValue.from(t.inputSchema().required()))
.putAdditionalProperty("additionalProperties", JsonValue.from(t.inputSchema().additionalProperties()))
.build())
.build())
.build())
.toList();
ChatCompletionCreateParams.Builder builder = ChatCompletionCreateParams.builder()
.model(model)
.maxCompletionTokens(1000)
.tools(chatTools)
.addUserMessage(query);
ChatCompletion response = openaiClient.chat().completions().create(builder.build());
ChatCompletion.Choice choice = response.choices().get(0);
while (choice.finishReason().equals(ChatCompletion.Choice.FinishReason.TOOL_CALLS)) {
ChatCompletionMessage msg = choice.message();
List<ChatCompletionMessageToolCall> calls = msg.toolCalls().orElse(List.of());
builder.addMessage(msg);
for (ChatCompletionMessageToolCall call : calls) {
String toolResult = callMcpTool(call.function().name(), call.function().arguments());
logger.info("Tool {} returned: {}", call.function().name(), toolResult);
builder.addMessage(ChatCompletionToolMessageParam.builder()
.toolCallId(call.id())
.content(toolResult)
.build());
}
response = openaiClient.chat().completions().create(builder.build());
choice = response.choices().get(0);
}
return choice.message().content().orElse("No response");
} catch (Exception e) {
logger.error("Unexpected error during processQuery", e);
return "[Error] " + e.getMessage();
}
}
private String callMcpTool(String name, String arguments) {
try {
McpSchema.CallToolRequest req = new McpSchema.CallToolRequest(name, arguments);
return toolToClient.get(name).callTool(req)
.content().stream()
.map(Object::toString)
.collect(Collectors.joining("
"));
} catch (Exception e) {
logger.error("Failed to call MCP tool {}: {}", name, e.getMessage());
return "[Tool Error] " + e.getMessage();
}
}
}Configuration
# application.properties
spring.ai.openai.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1
spring.ai.openai.api-key=sk-xxxx
spring.ai.openai.chat.options.model=qwen-max
mcp.servers=blender=http://localhost:8000
spring.main.web-application-type=none
spring.main.banner-mode=offRunning the Example
A Spring Boot controller exposes /client?query=…. Posting "Create a pink pig in Blender" triggers the following flow:
Tool list is fetched from the Blender MCP server.
OpenAI suggests calling execute_blender_code with Python code that builds a pig model.
The Java client forwards the code to the MCP server via SSE.
The server runs the script, creates the model, saves pig.blend, and returns a success message.
The final ChatGPT response is returned to the HTTP caller.
Sample Logs
INFO 127.0.0.1:51875 - "POST /messages/?session_id=fa4b... HTTP/1.1" 202 Accepted
INFO MyMCPClient - Tool execute_blender_code returned: Code executed successfully
INFO MyMCPClient - Tool save_scene returned: Scene saved to /Users/clong/Pictures/pig.blendThe resulting pig.blend file opens in Blender as a pink pig model.
Conclusion
The article demonstrates that Java can act as a first‑class client for MCP, bypassing the limitations of Spring AI. By using the raw MCP SDK together with OpenAI’s function‑calling feature, developers obtain a flexible, language‑agnostic bridge between Java back‑ends and Python AI services, enabling dynamic tool invocation, real‑time data retrieval, and seamless integration in enterprise applications.
Future Work
Replace SSE with WebSocket for true bidirectional streaming.
Add TLS and JWT authentication for secure MCP communication.
Integrate OpenTelemetry for observability.
Contribute a Spring‑Boot starter that correctly handles MCP payloads.
Alibaba Cloud Developer
Alibaba's official tech channel, featuring all of its technology innovations.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
