Tool Calling & MCP

How LLMs call your Java code โ€” function calling, the Model Context Protocol, and building MCP servers with Spring AI

← Back to Index

What is Tool Calling, and What Problem Does It Solve?

An LLM generates text. It cannot query your database, check an order status, call a payment gateway, or read a file โ€” it has no access to the outside world. Tool calling (also called function calling) is the mechanism that closes this gap: you describe available tools to the model, and the model responds with a structured request to invoke one โ€” including the arguments, extracted from the conversation. Your code executes the tool, returns the result, and the model incorporates it into its next response.

The model never executes anything. It decides which tool to call and what arguments to pass. Your application decides whether to actually execute it, validates the arguments, runs the code, and returns the result. The model is the decision-maker; your code is the executor.

/*
 * BEFORE โ€” a custom integration per external capability.
 * The LLM has no way to check order status, so you hardcode the lookup into
 * the prompt pipeline. Every new capability means new glue code, new prompt
 * rewiring, and a growing if/else chain that routes user intent to Java methods.
 */
if (userMessage.contains("order status")) {
    Order order = orderService.findByCustomerLastOrder(customerId);
    String prompt = "The customer's order #" + order.id()
        + " is " + order.status() + ". Respond to: " + userMessage;
    return chatClient.prompt().user(prompt).call().content();
} else if (userMessage.contains("return")) {
    // another branch, another hardcoded integration...
}
/*
 * AFTER โ€” tool calling. You describe the tools; the model decides when to use them.
 * Adding a new capability means adding a new @McpTool method โ€” no prompt rewiring,
 * no if/else routing, no glue code.
 */
@Component
public class OrderTools {

    private final OrderService orderService;

    public OrderTools(OrderService orderService) {
        this.orderService = orderService;
    }

    @McpTool(description = "Look up the current status of a customer order by order ID")
    public OrderStatusResponse getOrderStatus(
            @McpToolParam(description = "The order ID to look up") Long orderId) {
        Order order = orderService.findById(orderId);
        return new OrderStatusResponse(order.id(), order.status(), order.estimatedDelivery());
    }
}

public record OrderStatusResponse(Long orderId, String status, LocalDate estimatedDelivery) {}
Tool calling vs MCP โ€” the relationship

Tool calling is the capability: the model requests a function invocation, your code executes it. Every major LLM provider supports it via their API.

MCP (Model Context Protocol) is a standard that wraps tool calling into a protocol โ€” with discovery, schema negotiation, transport options, and a client/server architecture. It solves the problem of every provider and every tool having a different integration format. MCP is to tool calling what JDBC is to database access: a standard interface that decouples the consumer from the provider.

How Tool Calling Works โ€” The Loop

Tool calling is not a single request/response. It is a multi-turn loop between your application, the model, and your tools:

/*
 *  1. Your app sends the user message + a list of available tools (with schemas)
 *     to the model.
 *
 *  2. The model examines the message and decides:
 *     - Respond directly (no tool needed), OR
 *     - Request a tool call: { "tool": "getOrderStatus", "arguments": { "orderId": 4821 } }
 *
 *  3. Your app receives the tool call request, validates the arguments,
 *     executes the Java method, and sends the result back to the model.
 *
 *  4. The model incorporates the tool result into its response and replies
 *     to the user: "Your order #4821 is currently being shipped and should
 *     arrive by Friday."
 *
 *  5. The model may chain multiple tool calls in a single turn โ€” e.g. look up
 *     the order, then check the return policy, then compose a response.
 *
 *  USER โ”€โ”€โ–ถ APP โ”€โ”€โ–ถ MODEL
 *                     โ”‚
 *                     โ–ผ (tool call request)
 *            APP โ—€โ”€โ”€ MODEL
 *             โ”‚
 *             โ–ผ (execute tool, return result)
 *            APP โ”€โ”€โ–ถ MODEL
 *                     โ”‚
 *                     โ–ผ (final response)
 *  USER โ—€โ”€โ”€ APP โ—€โ”€โ”€ MODEL
 */
The model never executes your code

This is the most important thing to understand. The model outputs a JSON object saying "call getOrderStatus with orderId=4821." Your application decides whether to honour that request. You can validate arguments, enforce permissions, rate-limit, log, or refuse the call entirely. The model is a decision engine, not an executor โ€” your code retains full control.

MCP โ€” The Model Context Protocol

MCP is an open standard (created by Anthropic, now part of the Linux Foundation) that defines how AI applications discover and interact with external tools, data sources, and prompts. It replaces the ad-hoc integration pattern where every tool has a different schema and every provider has a different calling convention.

Architecture

Component Role Java example
Host The application the user interacts with (IDE, chat UI) Claude Desktop, an IntelliJ plugin
Client Maintains a 1:1 connection to one MCP server Spring AI's MCP client auto-configuration
Server Exposes tools, resources, and prompts via the MCP protocol Your Spring Boot app with @McpTool methods

What an MCP server exposes

Primitive What it is Example
Tools Functions the model can call โ€” with typed parameters and return values getOrderStatus(orderId), searchProducts(query)
Resources Read-only data the model can access โ€” files, database rows, API responses orders://{orderId} returns the order as JSON
Prompts Reusable prompt templates the client can fetch and fill A "summarise order" prompt template with parameter slots

Transport options

Transport When to use
STDIO Local process โ€” the host launches your JAR as a subprocess and communicates via stdin/stdout. No network, no port, no auth. Best for local development and CLI tools
Streamable HTTP Remote deployment โ€” the server is a regular HTTP endpoint. Supports stateless operation, load balancing, and standard web security. This is the default in Spring AI 2.0 and the MCP specification's recommended transport for production

Building an MCP Server with Spring AI

<!-- pom.xml โ€” MCP server starter -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
# application.properties
spring.ai.mcp.server.name=ecommerce-tools
spring.ai.mcp.server.version=1.0.0
// Tools are plain Spring beans annotated with @McpTool.
// Spring AI auto-discovers them, generates the JSON schema from the method
// signature, and registers them with the MCP server at startup.

@Component
public class ProductTools {

    private final ProductRepository productRepository;

    public ProductTools(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @McpTool(description = "Search products by name or category. Returns matching products with prices.")
    public List<ProductSummary> searchProducts(
            @McpToolParam(description = "Search query โ€” product name, category, or keyword")
            String query,
            @McpToolParam(description = "Maximum number of results", required = false)
            Integer maxResults) {

        int limit = (maxResults != null) ? maxResults : 10;
        return productRepository.searchByNameOrCategory(query).stream()
            .limit(limit)
            .map(p -> new ProductSummary(p.id(), p.name(), p.price(), p.category()))
            .toList();
    }

    @McpTool(description = "Get full details of a specific product by its ID")
    public Product getProductById(
            @McpToolParam(description = "The product ID") Long productId) {
        return productRepository.findById(productId)
            .orElseThrow(() -> new IllegalArgumentException("Product not found: " + productId));
    }
}

public record ProductSummary(Long id, String name, BigDecimal price, String category) {}
Tool descriptions are prompts โ€” write them carefully

The description on @McpTool and @McpToolParam is what the model reads to decide whether to call the tool and how to fill the arguments. A vague description like "gets product info" leads to wrong tool selection. A precise description like "Search products by name or category. Returns matching products with prices." tells the model exactly when to use it and what to expect back.

Exposing resources

@Component
public class OrderResources {

    private final OrderService orderService;

    public OrderResources(OrderService orderService) {
        this.orderService = orderService;
    }

    @McpResource(uri = "orders://{orderId}",
                 description = "Full order details including items, totals, and shipping address")
    public Order getOrder(Long orderId) {
        return orderService.findById(orderId);
    }
}

Client Side โ€” Consuming MCP Tools

An MCP client connects to one or more MCP servers, discovers their tools, and makes them available to the ChatClient. Spring AI auto-configures this.

# application.properties โ€” connect to a remote MCP server
spring.ai.mcp.client.streamable-http.connections.ecommerce.url=http://localhost:8080
// The ChatClient automatically sees tools from connected MCP servers.
// No manual wiring needed โ€” Spring AI resolves them at startup.
@Service
public class CustomerSupportService {

    private final ChatClient chatClient;

    public CustomerSupportService(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultSystem("""
                You are a customer support agent for an e-commerce platform.
                Use the available tools to look up order status, search products,
                and retrieve order details. Always verify information before
                responding to the customer.
                """)
            .build();
    }

    public String handleCustomerQuery(String customerMessage) {
        return chatClient.prompt()
            .user(customerMessage)
            .call()
            .content();
        // If the model decides it needs to check an order status or search
        // products, it will call the MCP tools automatically โ€” the loop
        // described in Section 1 happens inside .call().
    }
}
STDIO for local development

For local development โ€” including connecting your MCP server to Claude Desktop or Claude Code โ€” the STDIO transport is simpler. The host launches your JAR as a subprocess:

// claude_desktop_config.json
{
  "mcpServers": {
    "ecommerce": {
      "command": "java",
      "args": ["-jar", "path/to/ecommerce-tools.jar"]
    }
  }
}

Security โ€” Tools Are an Attack Surface

Every @McpTool method is code that an LLM can trigger. The model decides when to call it and what arguments to pass โ€” and those arguments come from user input interpreted by the model. This makes tool inputs fundamentally untrusted.

Risk Mitigation
Argument injection Validate all tool arguments the same way you validate @RequestBody โ€” null checks, range checks, regex patterns. The model can pass anything
Privilege escalation Tools should execute with the minimum permissions needed. A tool that looks up orders should not have write access to payment records. Apply the same principle of least privilege as any API endpoint
Data leakage Tool responses are sent to the model and may appear in the output. Never return sensitive data (passwords, payment details, PII) in a tool response unless the output channel is secured
Denial of service A tool that queries a database without limits can be called repeatedly by the model. Always paginate, set max results, and apply rate limits

Best Practices

โœ… Do write precise, unambiguous descriptions on @McpTool and @McpToolParam. The description is the only thing the model reads to decide whether to call the tool โ€” a vague description leads to wrong tool selection.

โœ… Do return records from tools. Spring AI serialises them to JSON via Jackson automatically, and the model can interpret the structured result cleanly.

โœ… Do validate all tool arguments as untrusted input. The model decides what to pass โ€” your code decides whether to accept it.

โœ… Do keep tools small and focused. A tool that does one thing well is easier for the model to select correctly than a tool that accepts a mode parameter and does five different things.

โœ… Do use STDIO for local development and Streamable HTTP for production deployment. STDIO requires no network, no port, and no auth โ€” the host launches your JAR directly.

โŒ Don't expose write operations (delete, update, payment) as tools without human-in-the-loop confirmation. The model can decide to call a tool that modifies state โ€” make destructive actions require explicit user approval.

โŒ Don't return sensitive data in tool responses. Everything returned from a tool is visible to the model and may appear in the output to the user.

โŒ Don't build one large tool with many optional parameters. The model selects better from several small, well-described tools than from one Swiss-army-knife tool with a complex parameter schema.

Interview Questions

๐ŸŽ“ Junior level

Q: What is tool calling in the context of LLMs?
Tool calling is a mechanism where the model, instead of generating a text response, outputs a structured request to call a specific function with specific arguments. The application executes the function and returns the result to the model, which uses it to compose the final response. The model never executes code โ€” it only requests execution.

Q: What is MCP and how does it relate to tool calling?
MCP (Model Context Protocol) is an open standard that defines how AI applications discover and interact with external tools, data sources, and prompts. It wraps tool calling into a protocol with discovery, schema negotiation, and standardised transport. Without MCP, every tool integration is custom. With MCP, tools are described once and work with any MCP-compatible client.

Q: What are the three primitives an MCP server can expose?
Tools โ€” functions the model can call (e.g. look up an order). Resources โ€” read-only data accessible by URI (e.g. order details). Prompts โ€” reusable prompt templates the client can fetch and fill.

๐Ÿ”ฅ Senior level

Q: Why should tool arguments be treated as untrusted input, even though they come from the model?
The model constructs tool arguments from user input interpreted through natural language. A user can craft a message that causes the model to pass malicious or unexpected values โ€” SQL fragments, negative quantities, IDs belonging to other users. Tool arguments should be validated with the same rigour as @RequestBody parameters on a public API endpoint: null checks, range validation, authorisation checks, and input sanitisation.

Q: What is the difference between STDIO and Streamable HTTP transport in MCP, and when would you use each?
STDIO runs locally โ€” the host launches the MCP server as a subprocess and communicates via stdin/stdout. No network, no port, no TLS, no auth. It is ideal for local development and CLI integration (e.g. Claude Desktop, Claude Code). Streamable HTTP exposes the MCP server as a regular HTTP endpoint, supporting stateless operation, load balancing, caching, and standard web security (OAuth, API keys). It is the recommended transport for production deployment and the default in Spring AI 2.0.

Q: How would you prevent a tool from being abused by the model in a production system?
Four layers: (1) validate all arguments โ€” reject out-of-range values, enforce authorisation per tool call, (2) rate-limit tool invocations to prevent the model from calling a tool in a tight loop, (3) make destructive operations require human confirmation โ€” the tool returns a confirmation token that a separate endpoint must approve before execution, (4) log every tool call with arguments and results for audit, and monitor for anomalous patterns.

Q: How does Spring AI 2.0 auto-register MCP tools, and what happens under the hood?
At startup, Spring AI scans for Spring beans with @McpTool methods. For each method, it reads the parameter types and @McpToolParam annotations to generate a JSON schema conforming to the MCP specification. It registers these schemas with the MCP server transport (STDIO or Streamable HTTP). When a client connects and requests tools/list, the server returns the full schema. When the model requests a tool call, the server deserialises the arguments, invokes the Java method via reflection, serialises the return value via Jackson, and sends it back. Records work directly as return types โ€” no adapter needed.