04-tool-calling-mcp-agent-evaluation

MCP: Sharing Tools Across Apps, Teams & Models

Expose your Java tools as an MCP server so any team or client can use them, and connect your own agent as an MCP client to tools you didn't write, with Spring AI's annotation-based MCP support.

August 13, 2026
spring-aimcpmcptoolinteroperabilitymodel-context-protocol

Two Teams, One Order-Lookup Tool

OrderTools.getRefundStatus from the first guide in this phase lives inside the support bot's codebase, as a @Component only that app can see. Then the Platform team, building an internal Slack bot for support staff, asks for the same capability — "can we look up refund status too?" The easy answer is copy-pasting OrderTools into their codebase. The correct answer is recognizing that's how two implementations quietly drift apart the first time the order schema changes.

Model Context Protocol (MCP), the open standard from Anthropic, exists for exactly this: expose a tool once, over a structured protocol, and let any MCP-compatible client — a different Java service, Claude Desktop, a Python agent, the Slack bot — call it without depending on your Java classes at all. It cuts both ways: your support bot can just as easily become a client, consuming tools someone else already built and exposed, instead of hand-writing an @Tool wrapper for every external capability it needs.


1. Exposing a Tool: Building an MCP Server

The annotation is @McpTool — deliberately similar to the @Tool annotation from the first guide, but scanned and exposed over the MCP protocol instead of registered directly on a ChatClient.

groovy
// build.gradle
implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
java
@Component
public class OrderMcpTools {
 
    private final OrderRepository orderRepository;
 
    public OrderMcpTools(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
 
    @McpTool(name = "getRefundStatus", description = "Look up the current status of a refund by order ID")
    public RefundStatus getRefundStatus(
        @McpToolParam(description = "The order ID", required = true) Long orderId
    ) {
        return orderRepository.findRefundStatus(orderId)
            .orElseThrow(() -> new IllegalArgumentException("No order found with ID " + orderId));
    }
}
yaml
# application.yml
spring:
  ai:
    mcp:
      server:
        annotation-scanner:
          enabled: true

That's the entire server. Spring Boot scans for @McpTool-annotated methods on startup, generates the same kind of JSON schema @Tool does, and serves it over HTTP — the Platform team's Slack bot (or Claude Desktop, or anything else speaking MCP) can now discover and call getRefundStatus without ever seeing OrderRepository or a line of your Java source.

spring-ai-starter-mcp-server-webmvc runs your server over streamable HTTP inside a normal Spring MVC app — the same app that already serves your REST controllers can serve MCP too. Reach for the plain spring-ai-starter-mcp-server starter instead for a STDIO-based server (a standalone process a client launches directly, common for local dev tools rather than a deployed service).


2. Consuming Tools: Building an MCP Client

Now the other direction — your support bot connecting out to tools it didn't write. Two connection types cover most real cases: STDIO for a locally-launched process (like the official filesystem server), and streamable HTTP for a deployed service (like an internal policy-docs MCP server run by another team).

groovy
// build.gradle
implementation 'org.springframework.ai:spring-ai-starter-mcp-client'
yaml
# application.yml
spring:
  ai:
    mcp:
      client:
        enabled: true
        stdio:
          connections:
            filesystem:
              command: npx
              args:
                - "-y"
                - "@modelcontextprotocol/server-filesystem"
                - "/data/policy-archive"
        streamable-http:
          connections:
            platform-orders:
              url: https://internal-mcp.acme.dev
              endpoint: /mcp

Two servers, two different transports, configured declaratively — no Java code needed just to establish the connections. Spring Boot auto-creates an McpSyncClient per configured connection and, from it, a SyncMcpToolCallbackProvider bean that wraps every discovered tool as a Spring AI ToolCallback.

java
@Service
public class SupportAgentService {
 
    private final ChatClient chatClient;
 
    public SupportAgentService(ChatClient.Builder builder, SyncMcpToolCallbackProvider mcpTools) {
        this.chatClient = builder
            .defaultToolCallbacks(mcpTools.getToolCallbacks())
            .build();
    }
}

mcpTools.getToolCallbacks() returns everything discovered across both configured servers — the filesystem tools and whatever platform-orders exposes — as ordinary ToolCallbacks. From here, they behave exactly like the @Tool-annotated methods from the first guide: the model decides when to call them, ToolCallingAdvisor runs the loop, and your application code never distinguishes "a local Java tool" from "a tool discovered from a remote MCP server."

⚠️

If two connected servers each expose a tool with the same name, Spring AI's DefaultMcpToolNamePrefixGenerator disambiguates them automatically (alt_1_toolName, alt_2_toolName) rather than silently letting one shadow the other — but a model picking between two similarly-named, similarly-described tools from different servers is still a real accuracy risk. Prefer distinct, descriptive tool names at the source rather than relying on the generated prefix to make the distinction obvious to the model.


3. Combining Local Tools and MCP Tools on the Same Agent

A ChatClient doesn't care whether a tool came from a local @Tool class or a remote MCP server — both are just ToolCallbacks by the time they reach ToolCallingAdvisor:

java
this.chatClient = builder
    .defaultTools(new CalculatorTools(), new EscalationTools(ticketService)) // local
    .defaultToolCallbacks(mcpTools.getToolCallbacks())                       // remote, via MCP
    .build();

One agent, calculating refunds locally, escalating locally, and pulling live order status or archived policy documents from two entirely separate teams' services — none of which needed to know this agent exists ahead of time.

Before wiring a new MCP server into your agent, connect to it with the MCP Inspector first — it's a standalone dev tool that lists a server's exposed tools and lets you call them manually, which is a much faster way to confirm a server behaves as expected than debugging it through a model's tool-calling decisions.


What's Next

Your agent can now act on its own tools and on tools it discovered from other teams and services. The last guide in this phase closes the loop on quality: how do you actually know an agent's answers — retrieved, tool-assisted, or both — are correct, before a customer or Legal finds out otherwise?

Frequently asked questions

Should I convert every @Tool in my app to @McpTool?

No — only tools another team, a different service, or an external client (like Claude Desktop) genuinely needs to reuse. A tool that's only ever called from within this one app's ChatClient doesn't need the protocol overhead of an MCP server; keep it a plain @Tool until reuse is an actual requirement, not a hypothetical one.

Does exposing a tool via MCP mean any MCP client can call it?

Only clients that can reach the server and are configured to connect to it — MCP itself doesn't add authentication. Put an MCP server behind the same access controls (network policy, API gateway, auth) you'd put around any other internal service; the protocol handles tool discovery and invocation, not who's allowed to invoke it.

What happens if an MCP server I depend on goes down?

Tool calls to it fail the same way any remote call failure does, and — per the error-handling discussion in the first guide of this phase — that failure becomes a RuntimeException message the model can react to, unless you've configured throw-exception-on-error otherwise. Treat an MCP client connection with the same reliability thinking (timeouts, fallback behavior) you'd apply to any other external service dependency.

Can an MCP server also be an MCP client?

Yes — nothing stops a single Spring Boot app from having both the server starter (exposing its own tools) and the client starter (consuming other servers' tools) at once. That's a legitimate pattern for a service that sits in the middle of a larger tool-sharing graph, though most single-purpose agents only need one side or the other.