TechByteByByte

Section 10 — MCP Integration

Connect Spring AI applications to Model Context Protocol tools and resources.

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 (ToolCallback registry) 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

TransportUse caseLifecycle
stdioLocal 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 HTTPRemote MCP servers over the networkStandard 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

  1. 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.
  2. Per-tenant tool scoping: in multi-tenant SaaS, which MCP servers/tools are available should typically be tenant-configurable — wire ToolCallbackProvider construction dynamically per-request/per-tenant rather than as one global static bean, mirroring the tenant-filter pattern from Section 6’s vector store guidance.
  3. 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.
  4. 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 whole ChatClient call.

10.9 Common Mistakes

  1. Assuming MCP tool execution is as fast as local @Tool execution — it crosses a process or network boundary; budget for meaningfully higher latency and design timeouts/retries accordingly.
  2. Not namespacing tool names across multiple MCP servers, causing ambiguous or colliding tool definitions presented to the model.
  3. Ignoring subprocess lifecycle in containerized deployments — orphaned stdio MCP server processes from improper signal handling.
  4. 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).
  5. Wiring one global ToolCallbackProvider in a multi-tenant system without per-tenant scoping, exposing tools a given tenant shouldn’t have access to.
  6. 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

  1. What’s the architectural relationship between MCP-sourced tools and @Tool-annotated local methods — do they converge on the same abstraction?
  2. Compare stdio and SSE/HTTP transports for MCP — what’s the lifecycle and ownership model for each?
  3. What production risk exists with stdio-transport MCP servers in containerized deployments, and how would you mitigate it?
  4. How would you handle tool-name collisions when integrating multiple MCP servers into one application?
  5. 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?
  6. Explain how a single @Tool-annotated method can be both locally invocable and externally MCP-discoverable without being written twice.
  7. What’s the enterprise pattern for aggregating many internal MCP servers behind one unified tool-consuming application?
  8. Why does per-tenant tool scoping matter in a multi-tenant SaaS MCP integration, and how would you implement it?
  9. 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?
  10. How would you debug an MCP stdio server that isn’t appearing in your application’s discovered tool list?
  11. What MCP protocol message triggers initial tool discovery, and when does re-discovery happen (or not happen) for dynamic tool sets?
  12. 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?
  13. How should timeout/retry configuration for an MCP-sourced tool differ from a local @Tool method, given the added process/network boundary?
  14. What’s the correct way to expose your own application’s tools as an MCP server without duplicating tool implementation code?
  15. Why is treating an internal network boundary as sufficient security for a remote MCP server often a mistake?
  16. Describe the MCP handshake sequence from subprocess spawn to tool availability.
  17. What happens to a spawned stdio MCP subprocess if the JVM is killed with SIGKILL instead of a graceful shutdown, and why does this matter operationally?
  18. How would you version and gracefully degrade against a schema-mismatch error from a third-party-maintained MCP server?
  19. What’s the practical latency difference you should budget for between an in-process @Tool call and an MCP-sourced tool call?
  20. How does McpToolCallbackProvider fit into the ToolCallback abstraction 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 @Tool methods.
  • 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 @Tool methods converge on the same ToolCallback abstraction — MCP is “tool calling, but out-of-process,” not a parallel system.
  • A @Tool method 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