Building a Production-Grade Tool Server for Banking Backends
bankingAugust 28, 2026

Building a Production-Grade Tool Server for Banking Backends

Spring AI + MCP

Every MCP tutorial ends the same way: a weather tool, a curl command, a green checkmark. None of them tell you what changes when the tool you're exposing isn't getWeather(city) but initiatePayment(accountId, amount), sitting behind a Spring Boot service your bank already trusts with real money. This one's for the Java engineer who's been asked to turn that service into something an LLM agent can call — and needs the parts the tutorial skipped. 


Spring AI's MCP support is genuinely good — annotation-driven tool registration, auto-configured transports, sync and async server types out of the box. It's also, by default, a POST /mcp endpoint with no authentication in front of it, happy to hand an LLM agent a JSON-RPC interface to whatever you've annotated. That's fine for a demo. It's not a production posture for a bank, and the gap between the two isn't something the starter closes for you — it's the actual engineering work. What follows is that work, done once so your team doesn't have to rediscover each piece the hard way. 


Transport: the spec has already moved past what most people ask for 


MCP originally shipped with HTTP+SSE as its remote transport, and if you're planning a build around SSE, it's worth knowing that the protocol has moved on. The 2025-03-26 MCP spec revision deprecated HTTP+SSE in favor of Streamable HTTP, for reasons that show up quickly once you try to run SSE at scale: the original transport needed two separate endpoints — one to receive server-sent events, one to post client messages — which forces awkward coordination between connections that are supposed to represent one logical session. Long-lived SSE connections are resource-intensive to hold open across a fleet of instances, there's no built-in way to resume a dropped connection mid-stream, and the transport doesn't get the benefits newer HTTP versions offer. Streamable HTTP fixes this by consolidating everything onto a single endpoint that upgrades to streaming only when a response actually needs it, with the resumability and bidirectional communication SSE never had. 


Spring AI supports both, and the choice is a one-line config change, not an architectural one — which is exactly why it's worth getting right the first time: 


 1 spring: 
 2   ai: 
 3     mcp: 
 4       server: 
 5         type: SYNC              # or ASYNC, for reactive tool implementations 
 6         protocol: STREAMABLE    # SSE | STREAMABLE | STATELESS 

spring-ai-starter-mcp-server-webflux or -webmvc gets you either transport; the property is what switches it. Default to STREAMABLE for anything new — it's the current spec's baseline, and it's what any MCP client built against a recent SDK will expect. Keep an SSE-configured instance around only if you have a specific client you don't control that hasn't upgraded yet, and treat it as a compatibility shim with a retirement date, not the primary interface. STATELESS is worth a specific look for a banking backend beyond either of those: it drops server-side session state entirely, which is what actually lets a tool server scale horizontally behind a standard load balancer without sticky sessions — a constraint most core banking infrastructure already has opinions about. 


Tool registries: curate the surface, don't just annotate what exists 

The annotation model makes exposing a method almost too easy: 

 1 @Component 
 2 public class PaymentTools { 
 3  
 4     @McpTool(name = "get_transaction_status", description = 
 5         "Look up the status of a previously submitted transaction by ID.") 
 6     public TransactionStatus getStatus( 
 7             @McpToolParam(description = "Transaction ID", required = true) String txId, 
 8             McpSyncRequestContext requestContext) { 
 9         String callerToken = (String) requestContext.transportContext() 
10             .get("authorization"); 
11         return transactionService.getStatus(txId, callerToken); 
12     } 
13 } 

 That's the right shape for a read-only tool. The trap is treating @McpTool as a convenient way to expose whatever's already sitting in a @Service class, one annotation at a time, until the tool registry has quietly become a mirror of your entire internal API surface. An MCP tool server for a banking backend should be built the way you'd build any other external-facing interface: a deliberate facade, narrower than what's behind it, where every tool is something you'd be comfortable explaining to an auditor as "yes, we intentionally let an LLM call this." That's the same ports-and-adapters instinct Java teams already apply at other integration boundaries — the tool registry is a boundary too, and it deserves the same discipline, not an exemption because the caller happens to be a model instead of another service. 


Two things follow from treating it that way. First, tool descriptions are part of your attack surface, not documentation — the entire tool schema, including parameter descriptions, is something a model reads and can be manipulated by, so it needs the same review as production copy, not whatever text was fastest to write. Second, per-request authorization has to run on the caller's actual identity, not the tool server's own service credential. The McpTransportContext pulling an Authorization header out of the incoming request, shown above, is what makes that possible — extract the caller's token at the transport layer and thread it into every downstream call, rather than letting the tool server hold one broadly-scoped credential and trust the agent to only ask for what it should. That distinction is what security guidance calls the confused deputy problem: a tool server that acts with its own privilege instead of enforcing the caller's is a privilege escalation waiting for the first agent session that gets manipulated into asking for something it shouldn't. 


Secrets handling: nothing hardcoded, nothing shared, nothing standing 

MCP's own security guidance is direct about where credentials go wrong in practice: tokens shared across servers instead of scoped per server, secrets sitting in plaintext config instead of a real secret store, long-lived personal-access-token-style credentials instead of short-lived ones scoped to a session. For a banking backend, none of that is a new problem — it's the same secrets-management discipline your existing services already answer to, and there's no reason the MCP boundary should get a pass. 

Concretely, that means: back tool-server credentials with whatever secret store your bank already runs (Vault or equivalent), not environment variables baked into a container image; use the MCP authorization flow — OAuth with PKCE, scoped narrowly per tool server rather than one shared client credential across every MCP server in your fleet — instead of static API keys; bind session identity to the actual authenticated user (user_id:session_id, validated on every request) so a session token can't be replayed against a different caller's context; and apply resource limits — rate limits, quotas, timeouts — per session, the same way you'd throttle any other client of a payments API. None of this is exotic. It's the standard OAuth-and-secrets-manager posture your team already runs elsewhere, applied to a new front door instead of invented from scratch for this one. 

The default-open trap is worth calling out explicitly, because it's the one every team hits once: Spring AI's own documentation states plainly that its HTTP-based transports expose an unauthenticated JSON-RPC endpoint by default, and that you must put a security boundary — Spring Security, or the community mcp-security project — in front of it before it's reachable beyond localhost. A tool server that works perfectly in local dev, behind no auth, is not a tool server that's ready to point at a payments service. That gap between "the demo works" and "this is safe to expose" is exactly where these projects lose weeks if it isn't planned for from the first sprint. 


The tool boundary needs the same guardrail discipline as any other AI-facing surface 

We've written before about why free text should never be the thing that moves money in an LLM-backed banking chatbot — the same principle applies at the tool server, just one layer earlier. A get_transaction_status tool is safe to let an agent call autonomously. An initiate_transfer tool is not, regardless of how well-scoped its OAuth token is — it needs an explicit human-approval step before execution, not just before the UI renders the result, and that approval has to be enforced by the tool server itself, not left to whatever the client application chooses to do with the response. Log every invocation — full parameters, caller identity, timestamp — into the same audit pipeline your other regulated systems already feed, for the same reason we've argued a DORA-grade audit trail needs to be independent of the system it's describing: "the agent called it and got a 200" is not an answer your risk function will accept for how a transfer happened, any more than "the model generated it and the tests passed" is an answer for how a line of production code came to exist. 

None of this is a reason to avoid building the tool server. It's the reason to build it deliberately: Streamable HTTP as the default transport, a curated tool registry instead of an annotated mirror of your service layer, caller-scoped credentials instead of one shared token, and a security boundary in front of the endpoint before anything beyond localhost can reach it. We've spent years putting Java backends in front of systems where a mistake shows up as a wire transfer, not a stack trace. An MCP tool server is a new kind of front door into that backend. It doesn't get to skip the discipline the rest of the door already has.