Spring AI MCP and Agents: Tools, Servers, Safe Loops
Learn how Spring AI uses the Model Context Protocol as client and server, runs bounded agent loops, and secures tool access with approvals and filters.
What you'll learn: how to expose your Spring services as MCP tools, consume other teams' MCP servers from a ChatClient, put hard limits and human approval around an agent loop, and lock down tool access so a model can only do what you intended.
In the previous lessons the model called Java methods that lived inside the same application. That works for one app. It stops working when a second team wants the same tools, when a desktop assistant needs to reach your order system, or when you want to plug in a tool someone else already built. The Model Context Protocol (MCP) solves that by turning "a set of tools" into a network service with a standard shape. Agents then use those tools in a loop, and that loop is where most of the risk lives.
The core idea
Think of MCP as the USB-C port of AI tooling. Before a standard port, every device needed its own cable. Before MCP, every AI application needed its own glue code to reach every system. With MCP, a system publishes its capabilities once, in a standard format, and any MCP-aware application can plug in.
Three roles matter:
MCP server: a process that publishes capabilities. It can publish tools (actions the model may request), resources (read-only data addressed by URI) and prompts (reusable prompt templates).
MCP client: the part of an application that connects to a server, asks what it offers, and invokes tools on request.
Host application: the app that owns the model conversation. In this lesson that is your Spring AI app, which uses an MCP client internally and hands the discovered tools to a ChatClient.
Spring AI builds on the official MCP Java SDK and adds Spring Boot starters and annotations on top. The important consequence is that an MCP tool, once discovered, looks to your ChatClient like any other tool. The model never talks to the MCP server. Your application does, exactly as with a local @Tool: the model asks, your code executes, and the result goes back to the model.
The model only ever sees tool names, descriptions and parameter schemas. Everything behind the three MCP servers stays inside your network boundary decisions, which is why the access controls later in this lesson matter so much.
What MCP servers can publish, and how they connect
Spring AI's MCP support covers the three capability types and several transports. The reference documentation describes the following.
Concept
What it is
Typical Spring AI mechanism
Tool
An action the model can request, with a JSON schema for its arguments
@McpTool on a method, or an existing ToolCallback bean
Resource
Read-only content addressed by a URI template
@McpResource
Prompt
A reusable, parameterised prompt
@McpPrompt
Transport
Use it when
Starter
STDIO
The server is a child process of the client, such as a local helper
A networked service shared by many clients; the normal choice for a team service
spring-ai-starter-mcp-server-webmvc or -webflux with spring.ai.mcp.server.protocol=STREAMABLE
Stateless Streamable HTTP
Cloud-native deployments where no session state should live in the instance
same starters with protocol=STATELESS
SSE
Older clients; the server docs mark it deprecated since 2.0.0 in favour of Streamable HTTP
same starters, protocol=SSE
A practical rule: use STDIO for a tool that only your process needs, Streamable HTTP for anything shared, and consider the stateless mode for microservice and cloud-native deployments, where the docs say no session state is kept between requests.
NOTE
Spring AI changes quickly. The names in this lesson (starters, properties, classes) were checked against the current Spring AI reference documentation, which was the 2.0 line when this lesson was written. Check the docs for the version your project uses, especially the package names of the MCP annotations, which this lesson leaves out of the imports on purpose.
Step 1: expose your own tools as an MCP server
Suppose the orders team owns order data. Instead of every AI app writing its own order lookup, the team publishes one MCP server.
Add the starter that matches your web stack (WebMVC shown):
spring:ai:mcp:server:name:orders-mcp-serverversion:1.0.0protocol:STREAMABLE# Streamable HTTP; STATELESS and SSE are the alternativestype:SYNC# SYNC is the default; ASYNC is the other optioninstructions:"Read-only order lookups. Use get-order-status before suggesting any action."request-timeout:20s
Then annotate methods. The annotation scanner is on by default, so any Spring bean with @McpTool methods is registered automatically. This is an excerpt: imports are omitted, and OrderRepository and OrderStatus are your own application types.
@ComponentpublicclassOrderTools {
privatefinal OrderRepository orders;
publicOrderTools(OrderRepository orders) {
this.orders = orders;
}
// Hints tell clients this tool only reads data. They are advisory, not enforcement.@McpTool(name = "get-order-status",
description = "Return the current status and delivery estimate for one order",
annotations = @McpTool.McpAnnotations(
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true))public OrderStatus getOrderStatus(
@McpToolParam(description = "Order id, for example A-1042", required = true) String orderId) {
return orders.findStatus(orderId)
.orElseThrow(() -> newIllegalArgumentException("No order with id " + orderId));
}
}
Two details deserve attention. First, the description is the model's only guide to when to call the tool, so write it for the model: what it returns, when to use it, what it does not do. Second, a RuntimeException thrown from a tool is converted into an error result that the model can read and react to, while checked exceptions and errors propagate and fail the call. Throw a clear IllegalArgumentException for bad input so the model can correct itself.
If you already have @Tool methods from the previous lesson, you do not have to rewrite them. The server starter detects ToolCallback and ToolCallbackProvider beans too, so a MethodToolCallbackProvider bean built from your existing service publishes those same methods over MCP.
WARNING
The HTTP transports expose an unauthenticated JSON-RPC endpoint (/mcp by default). Anyone who can reach it can list and call every tool. Do not expose the server beyond localhost until you have added Spring Security or the MCP security modules described later in this lesson. STDIO servers are not reachable over the network, so this warning does not apply to them.
spring:ai:mcp:client:type:SYNCrequest-timeout:30s# the default is 20s; slow tools need headroomstreamable-http:connections:orders:url:http://orders-mcp.internal:8080endpoint:/mcp# the default endpoint suffixstdio:connections:local-files:command:npxargs:-"-y"-"@modelcontextprotocol/server-filesystem"-"/srv/reports"
With tool-callback integration on (the default), the starter publishes the discovered tools as a ToolCallbackProvider bean. You hand it to the ChatClient for the request that needs it:
@ServicepublicclassSupportAssistant {
privatefinal ChatClient chatClient;
privatefinal ToolCallbackProvider mcpTools;
publicSupportAssistant(ChatClient.Builder builder, ToolCallbackProvider mcpTools) {
// Build from the injected builder so Spring AI's observability and customizers stay active.this.chatClient = builder
.defaultSystem("You are a support assistant. Use tools to look facts up. Never guess an order status.")
.build();
this.mcpTools = mcpTools;
}
public String answer(String question) {
return chatClient.prompt()
.user(question)
.tools(mcpTools) // per-request: appends to any default tools
.call()
.content();
}
}
Passing tools per request, rather than as client defaults, is deliberate. The reference documentation shows that default tools are available on every request built from that client, while tools passed with .tools(...) apply to one request and are appended to any defaults. Keeping risky or destructive tools on the per-request path is a design choice of this lesson, not a documented rule.
Controlling what the client sees
Remote servers are sources you do not fully control, and a server can add tools after you integrated it. Spring AI gives you two hooks on the client side:
McpToolFilter: a bean that receives the connection details and each tool, and returns whether to include it. Define one, and combine multiple rules inside it.
McpToolNamePrefixGenerator: by default, names are made unique across connections (non-alphanumeric characters become underscores, duplicates get a counter prefix). If you turn prefixing off with McpToolNamePrefixGenerator.noPrefix() and two servers publish the same tool name, startup fails with an IllegalStateException, which is a useful loud failure.
@ComponentpublicclassOrdersOnlyReadToolsimplementsMcpToolFilter {
@Overridepublicbooleantest(McpConnectionInfo connectionInfo, McpSchema.Tool tool) {
// Allow-list by naming convention; everything else is hidden from the model.return tool.name().startsWith("get-") || tool.name().startsWith("search-");
}
}
An allow-list is safer than a deny-list. If a server ships a new delete-everything tool next month, the allow-list ignores it without anyone remembering to update a block-list.
The sequence below shows one tool call end to end. Note how many hops are your code.
sequenceDiagram
participant U as User
participant A as Spring AI app
participant M as LLM provider
participant S as Orders MCP server
U->>A: Where is order A-1042?
A->>M: question plus tool schemas
M-->>A: request tool get-order-status with A-1042
A->>S: tools/call get-order-status
S-->>A: status shipped, estimate Friday
A->>M: tool result added to the conversation
M-->>A: final answer in plain language
A-->>U: Your order shipped and arrives Friday
From tool calls to agents
A single tool call is not an agent. An agent is a loop in which the model decides what to do next, calls tools, reads the results, and decides again until it considers the task finished. Spring AI's reference makes a distinction worth memorising: workflows orchestrate models and tools through code paths you define, while agents let the model direct its own process. The documentation's guidance is to start with the simplest pattern that works and add autonomy only when it measurably helps, because workflows are more predictable and easier to maintain.
The reference describes five workflow patterns that cover most enterprise needs without a free-running agent:
Pattern
Shape
Example use
Chain
Output of step one feeds step two
Draft a reply, then rewrite it in house style
Parallelization
Independent calls run side by side, results merged in code
Summarise ten documents at once
Routing
A classifier call picks a specialised handler
Billing question versus technical question
Orchestrator-workers
A model splits a task into subtasks for workers
Research that cannot be planned in advance
Evaluator-optimizer
One call generates, another critiques, loop until good
Content that must meet written criteria
Most of these are plain Java around several ChatClient calls. Reach for the free-running tool loop only when the number and order of steps truly cannot be known in advance.
How the loop runs inside Spring AI
When you register tools on a ChatClient, a ToolCallingAdvisor is registered automatically. It sends the request, executes any tool calls the model asks for through the ToolCallingManager, feeds the results back, and repeats until the model replies without a tool call. That loop is the agent.
Left unchecked, a loop can run away: the model keeps calling the same search tool with slightly different words, each call costing tokens and time. Spring AI's documented guardrails are per-turn call caps:
spring:ai:tools:limits:max-calls-per-tool-default:10# documented default is 40max-total-tool-calls:25# documented default is 150on-limit-exceeded:RETURN_ERROR_RESPONSE# THROW is the documented default
With THROW, the breach is returned to your code as the final response with a dedicated finish reason. With RETURN_ERROR_RESPONSE, the model is told the limit was reached and the conversation continues, which lets it wrap up gracefully. The reference does not document a separate "maximum iterations" setting, so these two caps are your loop bounds. Add a request timeout at the HTTP layer on top, as the production lesson explains.
Tool errors follow a similar rule. By default a RuntimeException message is sent back to the model so it can recover. If you prefer your code to see every failure, set spring.ai.tools.throw-exception-on-error=true.
flowchart TD
A["User request arrives"] --> B["Send prompt and tool schemas to the model"]
B --> C{"Did the model ask for tools?"}
C -- "No" --> Z["Return the final answer"]
C -- "Yes" --> D{"Call limits reached?"}
D -- "Yes" --> E["Stop: error response or exception"]
D -- "No" --> F{"Risky tool needs approval?"}
F -- "Yes" --> G["Pause for a human decision"]
G -- "Approved" --> H["Execute the tool"]
G -- "Denied" --> I["Return a denial message to the model"]
F -- "No" --> H
H --> J["Add result to the conversation"]
I --> J
J --> B
The two diamonds on the right-hand side, call limits and approval, are the controls you own. The rest of the loop is automatic.
Human-in-the-loop: approvals for consequential actions
Some tools are harmless to repeat (look up an order). Others are not (cancel an order, issue a refund, restart a server). For those, a person or a policy check should sit between "the model asked" and "the code ran".
Spring AI supports this with user-controlled tool execution. You switch off the automatic ToolCallingAdvisor for the request, then drive the loop yourself with ToolCallingManager. Each iteration is then yours to inspect, approve or deny. The following is adapted from the reference's pattern; imports are omitted and ApprovalPolicy is a hypothetical interface of your own.
public String answerWithApproval(ChatClient chatClient, ToolCallingManager toolCallingManager,
ToolCallback[] tools, String question, ApprovalPolicy policy) {
ChatOptionsoptions= ToolCallingChatOptions.builder().toolCallbacks(tools).build();
// Turn off the automatic loop for this request only.ChatClientResponseresponse= chatClient.prompt()
.user(question)
.options(options)
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call()
.chatClientResponse();
Promptprompt=newPrompt(List.of(newUserMessage(question)), options);
introunds=0;
while (response.chatResponse() != null && response.chatResponse().hasToolCalls()) {
if (++rounds > 5) {
return"Stopped: too many tool rounds."; // your own hard bound
}
// Ask a human or a policy engine. The ChatResponse carries the requested tool calls.if (!policy.allows(response.chatResponse())) {
return"That action needs approval. A request has been queued.";
}
ToolExecutionResultresult= toolCallingManager.executeToolCalls(prompt, response.chatResponse());
prompt = newPrompt(result.conversationHistory(), options);
response = chatClient.prompt()
.messages(result.conversationHistory())
.options(options)
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call()
.chatClientResponse();
}
return response.chatResponse().getResult().getOutput().getText();
}
Once you turn off the automatic advisor, your own code owns the loop (limits, error handling, tracing of each round), so use this only on the paths that need it, and keep the approval logic small. For most applications a simpler design also works: give the model only read tools, and have a tool like request-refund create a pending record that a human approves in a separate screen. The model proposes, a person disposes, and no loop surgery is required.
MCP adds one more mechanism on the client side. A server can ask the user for more information mid-task through elicitation, and Spring AI lets you handle that with an annotated method:
@ComponentpublicclassElicitationHandler {
// "orders" must match the connection name in your client configuration.@McpElicitation(clients = "orders")public ElicitResult handleElicitationRequest(ElicitRequest request) {
Map<String, Object> answers = presentFormToUser(request.requestedSchema()); // your UI codereturn answers != null
? newElicitResult(ElicitResult.Action.ACCEPT, answers)
: newElicitResult(ElicitResult.Action.DECLINE, null);
}
}
Securing tool access
Treat every tool as an API endpoint that an unpredictable, occasionally manipulated caller can reach. Four layers work together.
Network and authentication on the server. Put Spring Security in front of any HTTP MCP server. The Spring AI docs point to the MCP Security modules for this (org.springaicommunity:mcp-server-security for OAuth 2.0 resource-server and API-key support, mcp-client-security for client-side OAuth, and an authorization server module). The documentation labels these a community project, still work in progress, not officially endorsed, and with limits: the server module supports WebMVC with Streamable HTTP or stateless transport, not SSE or WebFlux, and expects JWT rather than opaque tokens. Read the limitations page before choosing it.
Authorization per tool. With method security enabled, @PreAuthorize works on @McpTool methods, so one tool can require a role while another is open to any authenticated caller. You can also leave initialize and tools/list public and secure only the calls.
Least privilege on the client. Use McpToolFilter allow-lists, and add destructive tools per request only for flows that earned them.
Pass identity, not trust. Use ToolContext to hand tenant and user ids to a tool without putting them in the prompt. The documentation states that this data is not sent to the model, so a prompt cannot override it.
@ServicepublicclassRefundTools {
// Requires method security: @EnableMethodSecurity on a configuration class.@PreAuthorize("isAuthenticated()")@McpTool(name = "request-refund",
description = "Create a PENDING refund request for human approval. Does not move money.")public String requestRefund(
@McpToolParam(description = "Order id", required = true) String orderId,
@McpToolParam(description = "Reason in one sentence", required = true) String reason) {
// Identity comes from the authenticated principal, never from model-supplied text.Stringcaller= SecurityContextHolder.getContext().getAuthentication().getName();
return refunds.createPending(orderId, reason, caller);
}
}
Finally, remember prompt injection through tool results. A tool that returns web pages, emails or tickets returns text written by third parties, and that text may contain instructions aimed at your model. Do not let such tools sit in the same loop as destructive tools without an approval step, and never rely on the model's good judgment as the only barrier.
Use cases
A shared order service for many assistants. One MCP server wraps order lookups. The web chatbot, an internal support copilot and an IDE assistant all consume it. Fixing a bug in the lookup logic fixes it everywhere, and one allow-list controls what each client sees.
Operations triage with approval gates. An on-call assistant gets read-only tools (service status, recent deployments, log search) from an MCP server. Remediation tools such as "restart service" are added per request and run through the human-approval loop above.
Local developer tooling. A STDIO server for the filesystem or a code index runs as a child process of a developer-facing application. No network port exists, so the unauthenticated-endpoint risk disappears, but you still limit which directories it can read.
Document workflows without a free agent. A routing workflow classifies an incoming document, then a chain extracts fields with a model call and validates them with an MCP-hosted rules tool. No open-ended loop is needed.
Choosing the right mechanism
Need
Local @Tool
MCP server
Plain REST call from code
Only this application needs it
Best fit
Overkill
Fine
Several apps or teams need the same tools
Duplicates code
Best fit
Needs a client per app
Third-party tools you did not write
Cannot
Best fit
Custom integration
Strict latency, no extra hop
Best fit
Adds a network hop
Fine
Non-AI callers also use it
Not reachable
Possible but unusual
Best fit
Deterministic path, no model choice
Not needed
Not needed
Best fit
flowchart TD
Q["Do you need the model to choose actions?"] -->|"No"| R["Call the API from code, use the model only to phrase text"]
Q -->|"Yes"| S{"Can the steps be fixed in advance?"}
S -->|"Yes"| T["Use a workflow: chain, routing or parallelization"]
S -->|"No"| U{"Does more than one app need these tools?"}
U -->|"No"| V["Local @Tool methods with call limits"]
U -->|"Yes"| W["Publish an MCP server and add auth"]
V --> X{"Any destructive tool?"}
W --> X
X -->|"Yes"| Y["Add approval or a pending-record pattern"]
X -->|"No"| Z2["Read-only tools: limits and logging are enough"]
In practice: an order-support assistant that can look things up but not move money
NOTE
A realistic scenario, not a case history. This is a composite example built from common production patterns. It does not describe any specific company, and every number in it is an illustrative assumption.
The situation. A retailer's support team wants an AI assistant that answers "where is my order?" and can start a refund. Two other teams (a mobile app and an internal agent desk) want the same order lookups. Compliance requires that no AI component can issue a refund by itself.
Starting architecture. Each of the three applications has its own copy of the order lookup code, written by a different team and slightly different in each. The chatbot calls its own @Tool methods, one of which can already post a refund directly.
Starting point: three apps, three copies of order logic, one tool that moves moneyWeb chatbot to Orders database; Mobile backend to Orders database; Agent desk app to Orders database; Web chatbot to Payments API (direct refund)Web chatbot: own @Tool copyWeb chatbotown @Tool copyMobile backend: own tool copyMobile backendown tool copyAgent desk app: own tool copyAgent desk appown tool copyOrders databaseOrdersdatabasePayments API: refund endpointPayments APIrefund endpointdirect refund
What changes.
The orders team publishes one Streamable HTTP MCP server with get-order-status and search-orders, both marked read-only, and request-refund, which only creates a pending record.
Spring Security with JWT validation sits in front of /mcp. request-refund carries @PreAuthorize, and the caller identity is read from the authenticated principal.
Each client app adds the MCP client starter, an McpToolFilter allow-list suited to its role (the mobile backend gets read tools only), and spring.ai.tools.limits caps.
The direct refund tool is deleted from the chatbot. A support agent approves pending refunds in the existing agent desk screen.
Tool calls appear in traces through the spring.ai.tool observation, so each refund request can be tied to a conversation.
After: one MCP server, per-client filters, refunds only as pending recordsWeb chatbot to Orders MCP server (MCP); Mobile backend to Orders MCP server (MCP); Orders MCP server to Orders database; Orders MCP server to Pending refunds (request-refund); Agent desk app to Pending refunds (review); Agent desk app to Payments API (approved only)Web chatbot: read + request toolsWeb chatbotread + requesttoolsMobile backend: read tools onlyMobile backendread tools onlyAgent desk app: approves refundsAgent desk appapproves refundsOrders MCP server: JWT securedOrders MCPserverJWT securedOrders databaseOrdersdatabasePending refunds: approval queuePendingrefundsapproval queuePayments APIPayments APIMCPMCPrequest-refundreviewapproved only
Result. Order lookup logic exists once, so a fix reaches every client at the same time. A model can no longer cause a refund: the worst a manipulated conversation can do is create a pending request that a person rejects. Tracing shows which conversation produced each request, and the call limits stop a looping model before it burns a large token budget. Adding a fourth client is a configuration change plus an allow-list, not new integration code.
Architect's notes
Start with fewer tools. Every tool schema is sent to the model on each request, which costs tokens and dilutes choice. A model with eight well-described tools usually behaves better than one with eighty. Group or hide tools per use case with filters.
Own the contract. An MCP server is an API. Version it, keep tool names stable, and treat description changes as behaviour changes, because the description is what the model reads. Ask consumers to pin what they allow-list.
Prefer workflows to free agents. If you can write the steps as Java, write them as Java. You get unit tests, predictable cost and clear failure modes. Use the open loop only for tasks that cannot be planned.
Make tools idempotent and boring. Models retry. A tool that creates an order twice when called twice is a bug waiting for a retry. Accept an idempotency key or make the action naturally repeatable.
Return compact results. A tool that returns a 50,000-token JSON blob wastes money and hides the answer. Return the fields the model needs.
Latency adds up. Each loop round trip is a model call plus a tool call. Three rounds on a slow model is a slow user experience, so stream the final answer and set tool timeouts deliberately.
Trust boundaries are real. A third-party MCP server is third-party code that influences your model's context. Review it as you would a dependency, and keep sensitive tools out of conversations that also use it.
Common mistakes
Exposing an HTTP MCP server with no authentication. The endpoint is open by default. Fix: add Spring Security (or the MCP security modules) before any non-local deployment.
Registering destructive tools as client defaults. Every request then carries them. Fix: add them per request, only on flows with an approval step.
Relying on readOnlyHint for safety. Hints are advisory metadata for clients. Fix: enforce read-only behaviour in the code and permissions behind the tool.
Trusting model-supplied identity. A tool that takes userId as a model argument can be told to use any user. Fix: take identity from the security context or ToolContext.
No bound on the loop. Defaults of 40 calls per tool and 150 in total are generous for most uses. Fix: set limits that match your real workflow.
Vague tool descriptions. The model guesses. Fix: describe purpose, inputs, outputs and limits in a sentence or two.
Building the ChatClient with ChatClient.create(...) in an app that needs observability. The reference notes this bypasses the autoconfigured builder. Fix: inject ChatClient.Builder.
Mixing sync and async MCP clients. The client docs state all clients must use the same type. Fix: choose SYNC or ASYNC once.
Key takeaways
MCP standardises how tools are published and discovered. Spring AI gives you starters for both server and client sides, and an MCP tool looks like any other tool to a ChatClient.
Choose the transport by audience: STDIO for local child processes, Streamable HTTP for shared services, stateless mode for scale-out, and avoid the deprecated SSE for new servers.
A tool description is part of your prompt. Write it carefully and keep tools few and focused.
An agent is a loop. Bound it with spring.ai.tools.limits, tool timeouts and your own round limits.
Prefer workflows (chain, routing, parallelization) when steps are known; keep free-running agents for open-ended problems.
Put humans or policies between the model and any consequential action, using user-controlled execution or a pending-record pattern.
Secure the endpoint, authorise each tool, filter what clients see, and take identity from the security context, not from the model.
Tool results are untrusted input. Never combine content-fetching tools and destructive tools without a gate.
Practice exercises
Starter (about 30 minutes). Create a Spring Boot project with the spring-ai-starter-mcp-server-webmvc starter. Publish one @McpTool that returns a fixed value, set protocol: STREAMABLE, and call it from the MCP Inspector or another MCP-capable client. Write the description twice (one vague, one precise) and note how the tool schema reads in the client.
Intermediate (about 2 hours). Build the consuming application from this lesson: the client starter, a streamable-http connection to your server, and a SupportAssistant that passes the ToolCallbackProvider with .tools(...). Add an McpToolFilter allow-list. Then set spring.ai.tools.limits.max-total-tool-calls to 3 and on-limit-exceeded to RETURN_ERROR_RESPONSE, craft a prompt that makes the model call tools repeatedly, and record what the model says when the limit is reached.
Advanced (half a day). Add Spring Security to the MCP server with JWT validation (follow the MCP Security page and read its limitations first). Mark a second tool, request-refund, with @PreAuthorize, and make it write a pending record only. Implement the approval loop from this lesson with a ApprovalPolicy that blocks any tool name starting with request- until a flag is set in a test. Turn on tracing and confirm each tool call appears as a spring.ai.tool observation. Write a short test that proves an unauthenticated call is rejected.
Lesson 5 of 7 · Spring AI: From Beginner to Expert
Learn how Spring AI uses the Model Context Protocol as client and server, runs bounded agent loops, and secures tool access with approvals and filters.
In the rest of this lesson
Step 2: consume MCP servers from a ChatClient
From tool calls to agents
Human-in-the-loop: approvals for consequential actions
Securing tool access
Use cases
Choosing the right mechanism
Enrolling is free. It unlocks every lesson in Spring AI: From Beginner to Expert, tracks your progress across devices and keeps your quiz scores. The newsletter is optional.
Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.
Get new articles by email
Free. We email a confirmation link first, and you can unsubscribe from any email.