Dev.to AI 🤖 Ai 👁 0 📖 6 min read

Beyond @Tool: Connecting Your Spring AI Agent to External Tool Servers

Every tool we've built so far lives inside the Spring Boot application. The @Tool method is in the same JAR as the agent. That works fine for small projects — but it doesn't scale well as the number of tools grows, and i

Every tool we've built so far lives inside the Spring Boot application. The @Tool method is in the same JAR as the agent. That works fine for small projects — but it doesn't scale well as the number of tools grows, and it means every tool change requires rebuilding and redeploying the agent.

The Model Context Protocol (MCP) changes this. It's a standard that lets tools live in a separate process — their own server — while the agent connects to them over a network. The agent doesn't care where the tool runs. It just calls it.

This article covers what MCP is, how to build an MCP server in Java, and how to connect it to a Spring AI agent.

What MCP actually is

MCP is a protocol — a defined format for how an AI agent discovers and calls tools that live outside it.

Without MCP, tools are tightly coupled to the agent:

agent → calls → @Tool method → inside the same JVM

With MCP, tools live in a separate server:

agent → HTTP/SSE → MCP server → runs the tool → returns result

The agent asks the MCP server: "what tools do you have?" The server responds with a list of tool names, descriptions, and parameter schemas — exactly like the @Tool and @ToolParam annotations define locally. The agent then calls those tools over the wire just as if they were local.

Why does this matter?

  • Tools written in different languages can serve the same agent
  • Tools can be updated and redeployed without touching the agent
  • Multiple agents can share the same MCP server
  • External teams can publish tools without access to your agent codebase

Building an MCP server with Spring AI

Spring AI includes built-in support for exposing tools as an MCP server. You add a dependency, annotate your tool class, and the framework handles the protocol.

Add the dependency:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
</dependency>

Configure it in application.properties:

spring.ai.mcp.server.name=travel-tools
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.type=SYNC

Write the tool class — same @Tool and @ToolParam annotations you already know:

@Service
public class TravelTools {

    @Tool(description = "Get the current weather for a city in India. " +
                        "Use when the user asks about weather or travel conditions.")
    public String getWeather(
            @ToolParam(description = "City name, e.g. Mysore") String city) {
        try {
            return weatherApi.fetch(city);
        } catch (Exception e) {
            return "Weather data unavailable for '" + city + "'. Try a nearby city.";
        }
    }

    @Tool(description = "Get the top tourist attractions in a city. " +
                        "Use when planning activities or asking what to visit.")
    public String getAttractions(
            @ToolParam(description = "City name") String city) {
        return attractionApi.fetch(city);
    }

    @Tool(description = "Get the cheapest ticket price between two cities in India. " +
                        "Use when asking about travel cost or how to get somewhere.")
    public String getTicketPrice(
            @ToolParam(description = "Departure city") String from,
            @ToolParam(description = "Destination city") String to) {
        try {
            return ticketApi.cheapest(from, to);
        } catch (NoRouteException e) {
            return "No direct route found between " + from + " and " + to + ".";
        }
    }
}

Register the tools as MCP tool callbacks:

@Configuration
public class McpServerConfig {

    @Bean
    public List<McpServerFeatures.SyncToolSpecification> tools(TravelTools travelTools) {
        ToolCallbackProvider provider =
            MethodToolCallbackProvider.builder()
                .toolObjects(travelTools)
                .build();

        return Arrays.stream(provider.getToolCallbacks())
            .map(callback -> new McpServerFeatures.SyncToolSpecification(
                callback.getToolDefinition(),
                (exchange, args) -> new McpSchema.CallToolResult(
                    List.of(new McpSchema.TextContent(
                        callback.call(String.valueOf(args))
                    )), false)
            ))
            .toList();
    }
}

Start the server. It exposes an SSE endpoint at /sse by default. Run it on port 8081.

server.port=8081

The MCP server is now running — it lists your tools and handles calls to them.

Connecting the agent to the MCP server

On the agent side (your Spring Boot application running on port 8080), add the MCP client dependency:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
</dependency>

Configure the connection:

spring.ai.mcp.client.sse.connections.travel-server.url=http://localhost:8081

Spring AI automatically discovers the tools from the MCP server at startup. You can see this in the log:

Discovered 3 tools from MCP server 'travel-server':
  - getWeather
  - getAttractions
  - getTicketPrice

Use the discovered tools in the agent — they arrive as ToolCallback beans:

@Component
public class TravelAgent {

    private final ChatClient chatClient;

    public TravelAgent(ChatClient.Builder builder,
                       List<McpSyncClient> mcpClients) {
        List<ToolCallback> mcpTools = new McpToolCallbackProvider(mcpClients)
            .getToolCallbacks();

        this.chatClient = builder
            .defaultSystem("You are a travel planning assistant for India.")
            .defaultTools(mcpTools)
            .build();
    }

    public String chat(String message, String conversationId) {
        return chatClient.prompt()
            .user(message)
            .advisors(a -> a.param(
                AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY,
                conversationId))
            .call()
            .content();
    }
}

The agent calls the MCP tools exactly as if they were local @Tool methods. The model sees the same tool names, descriptions, and parameter schemas. The network hop is invisible to the model.

The tool discovery lifecycle

When the agent starts, it connects to each configured MCP server and requests a list of available tools. This is the discovery step. The tool definitions are loaded once at startup and cached in the agent.

1. Agent starts
2. Agent connects to MCP server at http://localhost:8081/sse
3. Agent calls: tools/list
4. MCP server returns: [{name, description, inputSchema}, ...]
5. Agent registers these as available tools
6. User sends a message
7. Model selects a tool → agent calls MCP server: tools/call
8. MCP server runs the tool → returns result
9. Agent returns result to model → model generates response

If you add a new tool to the MCP server and restart it, the agent picks it up on its next restart (or reconnection). Tools can evolve independently of the agent.

Running multiple MCP servers

One agent can connect to multiple MCP servers — each responsible for a different domain:

spring.ai.mcp.client.sse.connections.travel-server.url=http://localhost:8081
spring.ai.mcp.client.sse.connections.payments-server.url=http://localhost:8082
spring.ai.mcp.client.sse.connections.docs-server.url=http://localhost:8083

The agent merges all discovered tools into one pool. When the model picks a tool, the agent routes the call to the correct server automatically — you don't write any routing logic.

This is powerful for larger systems: a payments team owns the payments MCP server, a content team owns the docs MCP server, and the agent team owns the agent. Each deploys and scales independently.

Error handling across the network

Local tool errors were simple — the tool returned a string. MCP tools can also fail at the network level: the server is down, the connection times out, the SSE stream drops.

Spring AI surfaces these as McpException. Catch them in your agent layer:

public String chat(String message, String conversationId) {
    try {
        return chatClient.prompt()
            .user(message)
            .call()
            .content();
    } catch (McpException e) {
        log.error("[agent] MCP error: {}", e.getMessage());
        return "One of the services I rely on is temporarily unavailable. " +
               "Please try again in a moment.";
    }
}

For resilience, add health checks on the MCP server connections — if a server is unreachable at startup, the agent should log a warning and continue without those tools rather than failing to start entirely.

MCP vs local @tool — when to use each

Local @Tool MCP server
Setup Annotate a method Separate Spring Boot app
Deployment Redeploy agent to change tools Deploy MCP server independently
Language Java only Any language with an MCP library
Latency Zero (same JVM) Network hop (usually <5ms local)
Sharing One agent Multiple agents
Use for Simple, agent-specific tools Shared tools, independent teams, non-Java tools

Start with local @Tool. Move to MCP when you have tools shared across agents, tools owned by other teams, or tools written in a different language.

What's next

The agent can now call tools that live anywhere on the network. The next question is quality — how do you know the agent is actually giving correct answers? The next article covers evaluating AI agent output: building test cases, scoring responses, and catching regressions before they reach users.

Sham Prakash K — Backend Engineer, 4+ years in Java, Spring Boot, and distributed systems. Building AI backend infrastructure. Writing about what I actually learned, mistakes included.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.