Building a Modern AI Bridge Spring AI, MCP, Python and Java 25
Building a Modern AI Bridge Spring AI, MCP, Python and Java 25
The Model Context Protocol (MCP) is rapidly becoming the standard for how AI models interact with external tools and data. By using Spring AI, we can expose Java services as tools that any MCP-compliant client—including those written in Python—can consume seamlessly.
In this guide, we'll walk through a Spring Boot setup that uses Java 25, Spring AI 2.0.0-M3, and a FastMCP Python client.
Note that any open ai specific dependency and chat client creation itself can be skipped in this. It was just meant for testing in java itself
1. The Foundation: pom.xml
To get started, we need the latest Spring AI starters. Note the use of the Spring AI Bill of Materials (BOM) to manage versions of the MCP server and OpenAI integration.
1<dependency>
2 <groupId>org.springframework.ai</groupId>
3 <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
4</dependency>
5<dependency>
6 <groupId>org.springframework.ai</groupId>
7 <artifactId>spring-ai-starter-model-openai</artifactId>
8</dependency>
2. Creating an "Observant" Chat Client
One of the most powerful patterns in Spring AI is the AugmentedToolCallbackProvider. This allows the AI to "think" before it acts. In the configuration below, we define an AgentThinking record. When the model decides to use a tool, it must also provide its reasoning and confidence level, which we then log on the server side.
1@Configuration
2public class Config {
3
4 @Bean
5 public ChatClient chatClient(ChatModel model) {
6 return ChatClient.builder(model)
7 .defaultSystem("You are a helpful assistant.")
8 .defaultToolCallbacks(AugmentedToolCallbackProvider.<AgentThinking>builder()
9 .toolObject(new DateTimeTools())
10 .argumentType(AgentThinking.class)
11 .argumentConsumer(event -> {
12 AgentThinking thinking = event.arguments();
13 log.info("Tool: {} | Reasoning: {} | Confidence: {}",
14 event.toolDefinition().name(),
15 thinking.innerThoughts(),
16 thinking.confidence());
17 })
18 .build())
19 .build();
20 }
21
22 record AgentThinking(
23 @ToolParam(description = "Your reasoning for calling this tool", required = true)
24 String innerThoughts,
25 @ToolParam(description = "Confidence level (low, medium, high)", required = true)
26 String confidence
27 ){}
28}
We are adding a simple DateTimeTools class to provide a few simple tools that the AI can use.
1package in.silentsudo.springmcptools.tools;
2
3import lombok.extern.slf4j.Slf4j;
4import org.springframework.ai.tool.annotation.Tool;
5import org.springframework.context.i18n.LocaleContextHolder;
6
7import java.time.LocalDateTime;
8
9@Slf4j
10public class DateTimeTools {
11
12 @Tool(description = "Returns the current date and time")
13 String getCurrentDateTime() {
14 log.info("Getting current date and time");
15 return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
16 }
17}
When you run this application with spring boot profile with following code, you can see the logs of the Getting current date and time tools getting called.
1@SpringBootApplication
2@RequiredArgsConstructor
3public class SpringMcpToolsApplication {
4
5 private static final Logger log = LoggerFactory.getLogger(SpringMcpToolsApplication.class);
6
7 public static void main(String[] args) {
8 SpringApplication.run(SpringMcpToolsApplication.class, args);
9 }
10
11 @Bean
12 public CommandLineRunner commandLineRunner(ChatClient chatClient) {
13 return args -> {
14 String response = chatClient.prompt("What is time now?")
15 .call().content();
16 log.info(response);
17 };
18 }
19}
The application are more than defining tools which are used in spring application. We can create mcp tools and expose them to the mcp client.
3. Defining MCP Tools
With Spring AI, turning a standard service into an MCP tool is as simple as adding the @McpTool annotation. This automatically metadata-tags the method so the MCP server can describe it to connected clients.
The following example defines a simple weather service that returns the current temperature for a given city. It is hardcoded to return a fixed value for testing purposes. You can hook up your logic here to call weather api and return the temperature.
1@Service
2public class WeatherService {
3
4 @McpTool(description = "Get current temperature for a location")
5 public String getTemperature(
6 @McpToolParam(description = "City name", required = true) String city) {
7 return String.format("Current temperature in %s: 22°C", city);
8 }
9}
application.properties for this is as below:
1spring.application.name=spring-mcp-tools
2spring.ai.openai.apiKey=sk-secret-key
3spring.ai.mcp.server.protocol=streamable
4logging.level.org.springframework.ai=DEBUG
5logging.level.org.springframework.web.servlet.mvc.method.annotation.SseEmitter=TRACE
6logging.level.io.modelcontextprotocol=DEBUG
4. Connecting from Python
The beauty of MCP is interoperability. Even though our server is Java-based, we can use the Python FastMCP library to discover and call these tools.
The following script connects to the Spring Boot application, lists the available tools (like our getTemperature tool), and prepares to invoke them.
create python venv using python3.10 -m venv venv and do pip install fastmcp
1import asyncio
2from fastmcp import Client
3
4async def main():
5 server_url = "http://localhost:8080/mcp"
6 print(f"Connecting to Spring AI at {server_url}...")
7
8 try:
9 async with Client(server_url) as client:
10 print("Successfully connected!")
11 tools = await client.list_tools()
12
13 for tool in tools:
14 print(f" - Discovered Tool: {tool.name}")
15
16 except Exception as e:
17 print(f"Python Client Error: {e}")
18
19if __name__ == "__main__":
20 asyncio.run(main())
When you run this script
1PYTHONDEVMODE=1 python main2.py
2Connecting to Spring AI at http://localhost:8080/mcp...
3Successfully connected!
4Discovered 1 tools:
5 - getTemperature
Why This Matters
- Decoupling: Your AI logic and tool definitions can live in a robust Java backend while your frontend or data science scripts live in Python.
- Observability: By using
AgentThinking, you gain insight into why an LLM chose a specific tool, making debugging agentic workflows much easier. - Future Proof: Using Java 25 and Spring AI 2.0.0-M3(NOT STABLE YET) ensures you are using the latest performance enhancements and virtual thread support.
Reference
Full XML Code
pom.xml
1<?xml version="1.0" encoding="UTF-8"?>
2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4 <modelVersion>4.0.0</modelVersion>
5 <parent>
6 <groupId>org.springframework.boot</groupId>
7 <artifactId>spring-boot-starter-parent</artifactId>
8 <version>4.0.4</version>
9 <relativePath/> <!-- lookup parent from repository -->
10 </parent>
11 <groupId>in.silentsudo</groupId>
12 <artifactId>spring-mcp-tools</artifactId>
13 <version>0.0.1-SNAPSHOT</version>
14 <name>spring-mcp-tools</name>
15 <description>spring-mcp-tools</description>
16 <url/>
17 <licenses>
18 <license/>
19 </licenses>
20 <developers>
21 <developer/>
22 </developers>
23 <scm>
24 <connection/>
25 <developerConnection/>
26 <tag/>
27 <url/>
28 </scm>
29 <properties>
30 <java.version>25</java.version>
31 </properties>
32 <dependencies>
33 <dependency>
34 <groupId>org.springframework.boot</groupId>
35 <artifactId>spring-boot-starter-webmvc</artifactId>
36 </dependency>
37 <dependency>
38 <groupId>org.springframework.ai</groupId>
39 <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
40 </dependency>
41 <dependency>
42 <groupId>org.springframework.ai</groupId>
43 <artifactId>spring-ai-starter-mcp-client</artifactId>
44 </dependency>
45 <dependency>
46 <groupId>org.springframework.ai</groupId>
47 <artifactId>spring-ai-starter-model-openai</artifactId>
48 </dependency>
49 <dependency>
50 <groupId>org.springframework.boot</groupId>
51 <artifactId>spring-boot-devtools</artifactId>
52 <scope>runtime</scope>
53 <optional>true</optional>
54 </dependency>
55 <dependency>
56 <groupId>org.projectlombok</groupId>
57 <artifactId>lombok</artifactId>
58 <optional>true</optional>
59 </dependency>
60 <dependency>
61 <groupId>org.springframework.boot</groupId>
62 <artifactId>spring-boot-starter-webmvc-test</artifactId>
63 <scope>test</scope>
64 </dependency>
65 <dependency>
66 <groupId>org.springframework.boot</groupId>
67 <artifactId>spring-boot-starter-actuator</artifactId>
68 </dependency>
69 </dependencies>
70
71 <dependencyManagement>
72 <dependencies>
73 <dependency>
74 <groupId>org.springframework.ai</groupId>
75 <artifactId>spring-ai-bom</artifactId>
76 <version>2.0.0-M3</version> <type>pom</type>
77 <scope>import</scope>
78 </dependency>
79 </dependencies>
80 </dependencyManagement>
81
82 <build>
83 <plugins>
84 <plugin>
85 <groupId>org.apache.maven.plugins</groupId>
86 <artifactId>maven-compiler-plugin</artifactId>
87 <configuration>
88 <annotationProcessorPaths>
89 <path>
90 <groupId>org.projectlombok</groupId>
91 <artifactId>lombok</artifactId>
92 </path>
93 </annotationProcessorPaths>
94 </configuration>
95 </plugin>
96 <plugin>
97 <groupId>org.springframework.boot</groupId>
98 <artifactId>spring-boot-maven-plugin</artifactId>
99 <configuration>
100 <excludes>
101 <exclude>
102 <groupId>org.projectlombok</groupId>
103 <artifactId>lombok</artifactId>
104 </exclude>
105 </excludes>
106 </configuration>
107 </plugin>
108 </plugins>
109 </build>
110 <repositories>
111
112 <repository>
113 <id>spring-snapshots</id>
114 <name>Spring Snapshots</name>
115 <url>https://repo.spring.io/snapshot</url>
116 <releases>
117 <enabled>false</enabled>
118 </releases>
119 </repository>
120 <repository>
121 <name>Central Portal Snapshots</name>
122 <id>central-portal-snapshots</id>
123 <url>https://central.sonatype.com/repository/maven-snapshots/</url>
124 <releases>
125 <enabled>false</enabled>
126 </releases>
127 <snapshots>
128 <enabled>true</enabled>
129 </snapshots>
130 </repository>
131 </repositories>
132
133</project>
Thanks!