Begin with the problem
Connecting AI tools through a shared protocol
Without a standard, every AI application needs custom wiring for every external tool. MCP defines common messages for discovering and calling tools, resources, and prompts across a process or network boundary.
Spring AI client ↔ MCP transport ↔ MCP server ↔ external system
What you will learn
- Explain MCP without confusing it with the model.
- Connect to and expose MCP capabilities.
- Compare local tools with remote MCP tools.
- Plan authentication, lifecycle, and failure handling.
Current official reference: Spring AI documentation for this topic. The examples below primarily preserve the stated 1.1.x course target. Where Spring AI 2.0 differs, the text must treat that behavior as version-specific rather than universal.
(Continues from Section 9. Target: Spring AI 1.1.x / Spring Boot 3.5.x. Note: the Spring AI 2.0 stable line includes MCP package and transport changes compared with 1.1.x; verify artifact names against the exact version in your build rather than copying milestone-era coordinates.)
10.1 Why MCP and Spring AI Tool Calling Are Complementary, Not Competing
Beginner primer: if MCP is new, read the glossary entry first. In short: MCP (Model Context Protocol) is an open, provider-agnostic standard for exposing tools, resources, and prompts to LLM applications — think of it as playing a role similar to how JDBC standardized talking to different databases, but for connecting an AI application to external tools/data sources maintained by anyone, written in any language.
Spring AI’s @Tool mechanism (Section 9) is in-process — the
tool is a Java method in your own codebase. MCP is the out-of-process equivalent:
tools live in a separate MCP server (possibly written in any language, possibly a
third-party integration you don’t own), and Spring AI’s MCP client machinery discovers
and wraps them as ToolCallbacks — the exact same ToolCallback abstraction from
Section 9, just sourced differently.
Real-world analogy — In-House Staff vs. Outsourced Contractors: @Tool methods are
employees on your payroll — you control their code, deploy them with your app. MCP tools
are outsourced contractors (a Slack integration vendor, a database-query service
maintained by another team) — you don’t control their internals, but you integrate with
them through a standard contract (MCP protocol) instead of a bespoke one-off integration
per contractor. Both ultimately show up on the same “who can I assign work to” roster
(ToolCallback list) that the LLM sees.
Analogy: In-House Staff vs. Outsourced Contractors Think of organizing operational workflows inside your enterprise department:
- In-House Staff (Local
@Tool/ Beans): You hire a team of local developers (Java methods in your repository). They sit in your office, deploy in your container, share your memory heap, and are written in Java. You manage their desks and schedules.- Outsourced Contractors (MCP Tools): You hire external agencies (Model Context Protocol servers). They live outside your office. One might write their tools in Python, another in Node.js, and another runs a legacy REST database system.
- You don’t rewrite their systems in Java. You talk to them using a standard contract interface (the MCP JSON-RPC protocol over Stdio or HTTP). The LLM simply views them as names on a unified dispatch board (
ToolCallbackregistry) and requests tasks from whoever is listed.
📊 Visual Flowchart: MCP Client Stdio Subprocess Connection Lifecycle
Here is the sequence of events during startup handshakes and runtime tool calls over stdio:
sequenceDiagram
autonumber
participant Spring as Spring AI Application
participant Process as OS Subprocess (MCP Server)
rect rgb(240, 240, 240)
Note over Spring: 1. Application Startup
Spring->>Process: Spawns subprocess (e.g., 'npx @modelcontextprotocol/server-filesystem')
Spring->>Process: Send JSON-RPC: "initialize" (handshake)
Process->>Spring: Respond: capabilities (tools list: read_file, write_file)
end
rect rgb(255, 250, 240)
Note over Spring: 2. ChatClient Runtime Call
Spring->>Process: Send JSON-RPC: "tools/call" (args: {"path": "/data/log.txt"})
Process->>Spring: Respond: tool result data string
end
rect rgb(240, 248, 255)
Note over Spring: 3. Application Shutdown
Spring->>Process: Close Stdio pipes (InputStream/OutputStream)
Spring->>Process: Send SIGTERM to subprocess
Note over Process: Subprocess exits gracefully
end
10.2 MCP Client — Consuming an External MCP Server
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
spring:
ai:
mcp:
client:
stdio:
connections:
filesystem-server:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
sse:
connections:
internal-crm-server:
url: https://mcp-crm.internal.acme.com
# auth headers configured here or via a custom
# WebClient.Builder bean interceptor for token refresh
@Bean
public ChatClient mcpEnabledChatClient(
ChatClient.Builder builder,
ToolCallbackProvider mcpToolCallbackProvider // auto-configured from the
// connections above
) {
return builder
.defaultToolCallbacks(mcpToolCallbackProvider.getToolCallbacks())
.build();
}
From here, the tool-calling execution path is identical to Section 9’s — the model sees
tool definitions, decides to call one, ToolCallingManager executes it. The only
difference is that execution, for an MCP-sourced tool, means the client sends a
tools/call MCP protocol message over the transport (stdio or SSE/HTTP) to the external
server instead of invoking a local Java method via reflection.
10.3 Transports
| Transport | Use case | Lifecycle |
|---|---|---|
| stdio | Local subprocess MCP servers (npm-packaged tools, local scripts) | Spring AI’s MCP client spawns and owns the subprocess; process lifecycle tied to application lifecycle |
| SSE / Streamable HTTP | Remote MCP servers over the network | Standard HTTP connection lifecycle; supports reconnection, works through normal network infra (load balancers, auth proxies) |
Note on 2.0.0-M3+: MCP transport artifacts were relocated as part of the 2.0 breaking-changes pass — if migrating from 1.x, consult the upgrade notes for the new artifact coordinates rather than assuming the 1.x dependency GAV still applies. This series targets 1.1.x where the artifact layout described above is current.
10.4 Lifecycle — stdio Server, Precisely
Application startup
│
▼
1. McpClientAutoConfiguration reads spring.ai.mcp.client.stdio.connections
│
▼
2. For each connection: ProcessBuilder spawns the configured command
(e.g., "npx -y @modelcontextprotocol/server-filesystem /data")
│
▼
3. MCP handshake over the subprocess's stdin/stdout: client sends
"initialize" request, server responds with its capabilities
(which tools/resources/prompts it exposes)
│
▼
4. Client sends "tools/list" — server responds with tool definitions
(name, description, JSON Schema) — these get wrapped as
ToolCallback instances by McpToolCallbackProvider
│
▼
5. Application runs normally; ChatClient calls may trigger "tools/call"
messages sent to the subprocess over stdio, response read back
│
▼
Application shutdown
│
▼
6. Spring AI closes the stdio streams and terminates the subprocess
as part of the ApplicationContext's bean destruction lifecycle —
an orphaned subprocess (not properly terminated) is a real
operational risk if lifecycle wiring is bypassed (e.g., a hard
kill -9 of the JVM instead of graceful shutdown)
Production risk worth internalizing: stdio-based MCP servers are child processes
of your application. In containerized deployments, ensure your container’s process
supervision (PID 1 handling, SIGTERM propagation) actually lets Spring’s shutdown
hooks run cleanly — a container killed abruptly can leave orphaned subprocesses if
signals aren’t forwarded correctly, which is a Docker/Kubernetes configuration concern
(proper ENTRYPOINT exec form, terminationGracePeriodSeconds) as much as a Spring AI
one.
10.5 Tool Discovery
@Bean
public ToolCallbackProvider mcpToolCallbackProvider(List<McpSyncClient> mcpClients) {
return McpToolCallbackProvider.builder()
.mcpClients(mcpClients)
.build();
}
Discovery happens at startup (the tools/list handshake in §10.4 step 4) — tool
availability is fixed for the application’s lifetime unless you explicitly implement
re-discovery (some MCP servers support a tools/list_changed notification for dynamic
tool sets, which requires reactive re-registration logic you’d build on top of the base
client, not something handled automatically without additional wiring).
Naming collisions: if two MCP servers (or an MCP server and a local @Tool) expose
tools with the same name, resolution behavior depends on registration
order/ToolCallbackProvider composition — production systems integrating multiple MCP
servers should namespace tool names deliberately (many MCP servers already prefix their
own tool names, but don’t assume this universally) or explicitly filter/rename during
ToolCallbackProvider construction to avoid ambiguous model-facing tool sets.
10.6 Authentication for Remote MCP Servers
SSE/HTTP-transport MCP servers need auth just like any other internal API call:
@Bean
public WebClient.Builder mcpWebClientBuilder(TokenProvider tokenProvider) {
return WebClient.builder()
.filter((request, next) -> {
String token = tokenProvider.getCurrentToken();
ClientRequest authorized = ClientRequest.from(request)
.header("Authorization", "Bearer " + token)
.build();
return next.exchange(authorized);
});
}
This WebClient.Builder bean is picked up by the MCP client auto-configuration the same
way a custom RestClient.Builder is picked up by ChatModel auto-configuration
(Section 1’s @ConditionalOnMissingBean/ObjectProvider pattern) — consistent
extension-point design across the framework, not a special MCP-specific mechanism.
10.7 MCP Server — Exposing Your Own Tools via MCP
Spring AI also supports the server side — exposing your application’s own tools as an MCP server other clients (Claude Desktop, other Spring AI apps, any MCP-compliant client) can consume:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
@Configuration
public class McpServerConfig {
@Bean
public ToolCallbackProvider orderToolsProvider(OrderTools orderTools) {
return MethodToolCallbackProvider.builder()
.toolObjects(orderTools)
.build();
}
}
The same @Tool-annotated methods from Section 9 become MCP-discoverable
automatically once exposed through the server starter — you don’t write tools
twice. A @Tool method can simultaneously be callable in-process by your own
ChatClient and externally discoverable by any other MCP client hitting your server
endpoint, because both paths converge on the same ToolCallback abstraction.
spring:
ai:
mcp:
server:
name: order-management-mcp-server
version: 1.0.0
transport: sse # or stdio for a server meant to be spawned as a subprocess
10.8 Enterprise MCP Patterns
- MCP Gateway/Aggregator: in an org with many internal MCP servers (CRM, inventory,
HR), a single aggregating MCP client application composes tools from all of them into
one unified
ToolCallbackProvider, presenting a consistent internal “AI-callable capability layer” to downstream chat applications — rather than every consuming application wiring up N individual MCP client connections itself. - Per-tenant tool scoping: in multi-tenant SaaS, which MCP servers/tools are
available should typically be tenant-configurable — wire
ToolCallbackProviderconstruction dynamically per-request/per-tenant rather than as one global static bean, mirroring the tenant-filter pattern from Section 6’s vector store guidance. - Observability across the MCP boundary: because MCP tool execution crosses a process/network boundary, standard Micrometer/OTel span propagation (Section 13) needs explicit attention — a tool call spanning your app → MCP server → the MCP server’s own downstream dependencies should ideally produce one connected trace, not three disconnected ones; this requires the MCP server side to also participate in distributed tracing context propagation, which depends on the specific server’s own instrumentation, not something Spring AI’s client can force onto a third-party server it doesn’t control.
- Contract stability: because MCP servers are often maintained by a different team
(or vendor), treat their tool schemas like any external API contract — version them,
and design your
ToolExecutionExceptionProcessor(Section 9) to handle a schema-mismatch/protocol-version error gracefully rather than crashing the wholeChatClientcall.
10.9 Common Mistakes
- Assuming MCP tool execution is as fast as local
@Toolexecution — it crosses a process or network boundary; budget for meaningfully higher latency and design timeouts/retries accordingly. - Not namespacing tool names across multiple MCP servers, causing ambiguous or colliding tool definitions presented to the model.
- Ignoring subprocess lifecycle in containerized deployments — orphaned stdio MCP server processes from improper signal handling.
- No authentication on remote SSE MCP servers, treating an internal network boundary as sufficient security (it usually isn’t, especially in zero-trust internal network models).
- Wiring one global
ToolCallbackProviderin a multi-tenant system without per-tenant scoping, exposing tools a given tenant shouldn’t have access to. - No distributed tracing continuity across the MCP boundary, making cross-process debugging of a failed tool call much harder than it needs to be.
10.10 Debugging
logging:
level:
org.springframework.ai.mcp: DEBUG
io.modelcontextprotocol: DEBUG
For stdio servers specifically, temporarily run the exact configured command manually in
a terminal (npx -y @modelcontextprotocol/server-filesystem /data) to isolate whether a
failure is in the MCP server itself versus Spring AI’s client wiring — this single step
resolves a large fraction of “my MCP tool isn’t showing up” issues by ruling out the
subprocess side entirely.
10.11 Interview Questions
- What’s the architectural relationship between MCP-sourced tools and
@Tool-annotated local methods — do they converge on the same abstraction? - Compare stdio and SSE/HTTP transports for MCP — what’s the lifecycle and ownership model for each?
- What production risk exists with stdio-transport MCP servers in containerized deployments, and how would you mitigate it?
- How would you handle tool-name collisions when integrating multiple MCP servers into one application?
- What authentication mechanism would you apply to a remote SSE-transport MCP server, and where does that configuration hook into Spring AI’s auto-configuration model?
- Explain how a single
@Tool-annotated method can be both locally invocable and externally MCP-discoverable without being written twice. - What’s the enterprise pattern for aggregating many internal MCP servers behind one unified tool-consuming application?
- Why does per-tenant tool scoping matter in a multi-tenant SaaS MCP integration, and how would you implement it?
- What distributed tracing challenge is specific to MCP tool execution, and why can’t Spring AI’s client alone guarantee end-to-end trace continuity?
- How would you debug an MCP stdio server that isn’t appearing in your application’s discovered tool list?
- What MCP protocol message triggers initial tool discovery, and when does re-discovery happen (or not happen) for dynamic tool sets?
- What breaking changes did Spring AI 2.0.0-M3 introduce for MCP integration, and why would that matter for a team planning a version upgrade?
- How should timeout/retry configuration for an MCP-sourced tool differ from a local
@Toolmethod, given the added process/network boundary? - What’s the correct way to expose your own application’s tools as an MCP server without duplicating tool implementation code?
- Why is treating an internal network boundary as sufficient security for a remote MCP server often a mistake?
- Describe the MCP handshake sequence from subprocess spawn to tool availability.
- What happens to a spawned stdio MCP subprocess if the JVM is killed with
SIGKILLinstead of a graceful shutdown, and why does this matter operationally? - How would you version and gracefully degrade against a schema-mismatch error from a third-party-maintained MCP server?
- What’s the practical latency difference you should budget for between an in-process
@Toolcall and an MCP-sourced tool call? - How does
McpToolCallbackProviderfit into theToolCallbackabstraction established in Section 9?
10.12 Best Practices Checklist
- Namespace or explicitly resolve tool-name collisions when integrating multiple MCP servers.
- Ensure containerized deployments propagate termination signals correctly to avoid orphaned stdio MCP subprocesses.
- Authenticate every remote SSE/HTTP MCP server connection; never rely on network-boundary-only security.
- Scope tool availability per-tenant in multi-tenant systems rather than one global static provider.
- Budget meaningfully higher timeouts for MCP-sourced tool calls versus local
@Toolmethods. - Treat third-party MCP server schemas as versioned external contracts with graceful degradation on mismatch.
- Verify MCP artifact coordinates against your target Spring AI version given the 2.0.0-M3 package/transport relocations.
10.13 Key Takeaways
- MCP tools and local
@Toolmethods converge on the sameToolCallbackabstraction — MCP is “tool calling, but out-of-process,” not a parallel system. - A
@Toolmethod can be both locally invocable and externally MCP-discoverable simultaneously, with zero duplication. - The process/network boundary MCP introduces has real operational consequences: latency, subprocess lifecycle, authentication, and distributed tracing continuity all need deliberate handling that local tool calling doesn’t.
- Enterprise MCP adoption benefits from an aggregation/gateway pattern rather than every consuming application wiring up every MCP server independently.
- Treat externally-maintained MCP servers’ schemas as versioned contracts, not stable internal code you control.
End of Section 10. Next: Section 11 — Structured Output (JSON, Schema Validation, POJO Mapping, Records, Enums, Error Recovery).
- Author
- TechByteByByte Editorial Team
- Reviewed by
- TechByteByByte Admin
- Published
- Last reviewed