aregmi.net
Resume

Letting the LLM call your Java methods with @Tool and ToolContext

tools function-calling spring-ai mcp actions

Tools

The pattern

Here's a clean tool flow:

  1. Define small tools (TimeService, CustomerService)
  2. Register tools in the prompt call
  3. Pass sensitive IDs through toolContext (not through prompt text)

That is the right pattern for going from demo to production.

Why does the LLM need tools?

LLMs are smart but stuck in time. They don't know today's date, can't check the weather, can't query your database. Tools fix that — they let the model ask your app to do things, and your app does them.

The key thing: the model never directly calls any API. It asks your app, your app runs the tool, returns the result. The LLM just decides what to call.

sequenceDiagram
    participant User
    participant App
    participant LLM
    participant Tool
    User->>App: "What's the weather in Denver?"
    App->>LLM: prompt + tool definitions
    LLM->>App: tool_call: getWeather("Denver")
    App->>Tool: execute getWeather("Denver")
    Tool->>App: 72°F, sunny
    App->>LLM: tool result: 72°F, sunny
    LLM->>App: "It's 72°F and sunny in Denver!"
    App->>User: response

Level 1: @Tool annotation

Simplest way to make a tool. Annotate a method, pass the class to ChatClient, done.

@Component
class TimeService {

    @Tool(name = "timeTool", description = "This tool returns the current time")
    public String timeTool() {
        return "The current time is: " + java.time.LocalTime.now();
    }

    @Tool(name = "dateTool", description = "This tool returns today's date.")
    public String dateTool() {
        return "Today's date is: " + java.time.LocalDate.now();
    }
}

Now use it:

String response = chatClient.prompt("What time is it right now?")
    .system("You're a helpful assistant. Use tools when needed.")
    .tools(new TimeService())
    .call()
    .content();

Without the tool? The model says "I don't have access to real-time information." With the tool, it just works.

Level 2: ToolContext for passing data securely

This is the good part — passing userId via ToolContext instead of stuffing it into the prompt.

@Component
class AccountService {

    @Tool(name = "userProfile", description = "Get user profile information by ID")
    public String getUserProfile(ToolContext toolContext) {
        String userId = (String) toolContext.getContext().get("userId");
        if ("12345".equals(userId)) {
            return "Name: Alex Doe, Device: smartphone";
        }
        return "User profile not found for ID: " + userId;
    }
}
String answer = chatClient.prompt(prompt)
    .tools(new AccountService(), new TimeService())
    .toolContext(Map.of("userId", userId))
        .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
        .call()
        .chatResponse()
        .getResult().getOutput().getText();

If you need to keep PII out of the model, this is the move.

Level 3: Keep tools simple

Simple tools win. Here's what that looks like:

@Tool(name = "timeTool", description = "This tool returns the current time")
public String timeTool() { ... }

@Tool(name = "dateTool", description = "This tool returns today's date")
public String dateTool() { ... }

@Tool(name = "userProfile", description = "Get user profile information by ID")
public String getUserProfile(ToolContext toolContext) { ... }

Does this look too basic? Good. Tools work best when they do one thing clearly.

Things to remember

  1. Descriptions matter — the model uses them to decide when to call a tool
  2. One tool, one job — keep them focused
  3. Use ToolContext for IDs and sensitive values
  4. Pair tools with memory (conversationId) for real assistant behavior