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.

Alibaba Cloud Developer
Alibaba Cloud Developer
Alibaba Cloud Developer
Bridge Java and Python AI: Integrate MCP with Spring AI for Seamless Tool Calls

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

Running 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.blend

The 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.

MCP workflow diagram
MCP workflow diagram
backendJavaMCPSpring AIAI integrationSSEtool calls
Alibaba Cloud Developer
Written by

Alibaba Cloud Developer

Alibaba's official tech channel, featuring all of its technology innovations.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.