Your Spring Boot service is already an AI tool server
The AI tooling ecosystem assumes you write Python. Every tutorial, every SDK example, every “build an agent in 20 lines” post. If your production systems are Java (mine are, and so are most of the enterprise backends actually running money and regulated data), the implicit message is that AI integration happens somewhere else, in some other codebase, maintained by some other team.
That was true for about eighteen months. It isn’t anymore.
The Model Context Protocol is a wire protocol, not a Python library. It speaks JSON-RPC over stdio or SSE. Anything that can read stdin and write stdout can implement it, which means your existing Spring @Service beans are roughly three annotations away from being callable by an LLM.
I built one to find out where the sharp edges are. This post is what I’d have wanted to read first.
What I built
open-banking-mcp: a Spring Boot 4.1 + Spring AI 2.0 server that exposes personal banking data as AI-callable tools. Spring AI 2.0 went GA in June 2026 on a Spring Boot 4 baseline, and it folded the MCP transport implementations into the framework itself rather than leaving them in the MCP Java SDK. That matters: MCP is no longer a bolt-on in the Spring world, it’s part of the platform. Connect it to Claude Desktop and ask, in plain language:
How much did I spend on groceries last month?
The model discovers the available tools, decides it needs a spending summary, calls it, and answers. No routing logic, no intent classification, no prompt templates on my side. Four tools:
| Tool | Purpose |
|---|---|
listAccounts |
Accounts with IBAN, type, alias, balance |
getTransactions |
Transactions for an account in a date range |
searchTransactions |
Keyword search over the last 90 days |
spendingSummary |
Per-category spend for a calendar month |
It runs in sandbox mode by default against deterministic synthetic data: no credentials, no real bank, nothing to leak. A live PSD2 client sits behind the same interface as a planned second profile.
The parts that are boring
This is genuinely most of it.
One dependency. Check the exact coordinates against the Spring AI 2.0 reference docs, because the starter naming has changed across releases and half the tutorials online still quote the old one:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId><!-- MCP server starter, per 2.0 docs --></artifactId>
</dependency>
Annotate ordinary methods:
@Service
public class BankingTools {
private final BankingClient client;
@Tool(description = """
Summarize spending by category for one calendar month.
Month must be in yyyy-MM format. Requires a valid accountId;
call listAccounts first if you do not have one.
""")
public SpendingSummary spendingSummary(String accountId, String month) {
return SpendingAnalyzer.categorize(
client.fetchTransactions(accountId, month));
}
}
Register them:
@Bean
public ToolCallbackProvider bankingToolProvider(BankingTools tools) {
return MethodToolCallbackProvider.builder()
.toolObjects(tools)
.build();
}
Configure the transport:
spring:
ai:
mcp:
server:
name: open-banking-mcp
version: 0.1.0
transport: STDIO
Package it, point claude_desktop_config.json at the jar, restart. That’s the entire integration. Spring AI generates the JSON Schema for each tool from the method signature and handles the protocol handshake, tool discovery, and marshalling.
If you already have a service layer, you are not writing an AI application. You are adding metadata to code you shipped years ago.
The four things that cost me time
1. stdout is the transport
Under STDIO, the JSON-RPC stream and your application’s standard output are the same file descriptor. A Spring banner, a stray System.out.println, a logging appender left on console: any of it corrupts the stream, and the client fails with a parse error that points nowhere near the actual cause.
spring:
main:
banner-mode: off
logging:
pattern:
console: # empty, nothing goes to stdout
file:
name: ${user.home}/.open-banking-mcp/server.log
Debug against the log file, never the console. This is the single most common way a working server looks broken.
2. Tool descriptions are the interface contract
In a REST API, the consumer reads your OpenAPI spec and a human writes the client. Here the consumer is the model, and the description string is the only documentation it will ever see. Vague descriptions produce wrong calls, and you will misdiagnose them as model failures.
Treat the description as API design:
- State the exact format of every parameter (
yyyy-MM, not “a month”) - State preconditions and what to call first
- State what the tool does not do, if that’s a plausible confusion
The difference between a tool the model uses correctly and one it fumbles is usually two sentences of English, not any change in Java.
3. Fail loudly, with a recovery hint
Standard backend instinct is to return a clean error and let the caller deal with it. Here the caller can reason. An exception message is an instruction it can act on within the same turn:
throw new IllegalArgumentException(
"Unknown accountId '" + id + "'. Call listAccounts to get valid ids.");
The model reads that, calls listAccounts, retries, and answers the user’s original question. A generic 404 ends the conversation instead. Error messages become part of the control flow, which is not a sentence I expected to write about a Spring service.
4. Determinism is a testing requirement, not a nicety
The sandbox client generates transactions from a fixed seed. Same data every run, so demos are reproducible, screenshots stay valid, and, more importantly, the tools are testable as ordinary Spring beans with no LLM in the loop at all.
@Test
void spendingSummary_aggregatesByCategory() {
var summary = tools.spendingSummary("GR16-0110-DEMO", "2026-06");
assertThat(summary.byCategory()).containsEntry("GROCERIES", 412.80);
}
The layer people assume is untestable because it involves AI is, in fact, the most testable part. The non-determinism lives in the client, on the other side of the protocol boundary. Your side is pure functions with annotations.
What this means for Java shops
The interesting consequence isn’t the chatbot. It’s that MCP inverts the integration direction. Instead of building an AI feature into your product (new endpoints, new UI, new prompt management, a new failure surface in your critical path), you expose the capabilities you already have and let a client outside your system compose them.
For regulated environments that distinction matters. The model never touches your database. It calls a tool with a typed signature, over a boundary you control, with authorization enforced exactly where it always was. Nothing about your audit story changes.
The catch, honestly: MCP is young and the specification still moves. Spring AI 2.0 closed the biggest gap (Streamable HTTP replaced the deprecated SSE transport as the default, and OAuth 2.0 and API-key security exist for remote deployments), but the surrounding practice is thin. Almost every tutorial you’ll find, including the ones ranking highest today, targets Spring AI 1.x and will mislead you on the 2.0 API. Read the reference docs, not the blogs. Including this one, in six months.
Try it
open-banking-mcp isn’t public yet, I’m cleaning it up before pushing it out. If you want a head start in the meantime: the four steps above (dependency, @Tool methods, a ToolCallbackProvider bean, STDIO transport config) are the entire skeleton. Point it at synthetic data first, the way I did, and the sharp edges in “the four things that cost me time” are the ones worth building toward on purpose.
If you maintain a Spring service and have been assuming AI integration is someone else’s stack, spend an afternoon on this. The gap between your existing code and an AI-callable tool server is much smaller than the ecosystem’s Python-shaped marketing suggests.