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 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
*/
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) {}
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().
}
}
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
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.
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.