Skip to content

    Spring AI

    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.

    By Published 17 min read

    Share

    This lesson is part of Spring AI: From Beginner to Expert. Enroll for free, then sign in, to read it in full.

    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.

    A Spring AI application consuming three MCP servers over different transportsSpring AI app to LLM provider (prompt + tool schemas); Spring AI app to Orders MCP server (Streamable HTTP); Spring AI app to Third-party MCP server (Streamable HTTP); Spring AI app to Local MCP server (stdio); Orders MCP server to Orders database; Third-party MCP server to Vendor API; Local MCP server to Project filesSpring AI app: ChatClient + MCP clientSpring AI appChatClient + MCPclientLLM provider: sees tool schemas onlyLLM providersees tool schemasonlyOrders MCP server: @McpToolOrders MCPserver@McpToolThird-party MCP server: remoteThird-partyMCP serverremoteLocal MCP server: child processLocal MCPserverchild processOrders databaseOrdersdatabaseVendor APIVendor APIProject filesProject filesprompt + tool schemasStreamable HTTPStreamable HTTPstdio

    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 spring-ai-starter-mcp-server (server), spring-ai-starter-mcp-client (client)
    Streamable HTTP 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):

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

    Configure it:

    spring:
      ai:
        mcp:
          server:
            name: orders-mcp-server
            version: 1.0.0
            protocol: STREAMABLE        # Streamable HTTP; STATELESS and SSE are the alternatives
            type: SYNC                  # SYNC is the default; ASYNC is the other option
            instructions: "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.

    @Component
    public class OrderTools {
    
        private final OrderRepository orders;
    
        public OrderTools(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(() -> new IllegalArgumentException("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.

    For a deeper walk-through of the server side, including Claude Desktop and IDE clients, see Building an MCP Server with Spring AI.

    Step 2: consume MCP servers from a ChatClient

    Now the consuming application. Add the client starter and describe each connection by name:

    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-starter-mcp-client</artifactId>
    </dependency>
    spring:
      ai:
        mcp:
          client:
            type: SYNC
            request-timeout: 30s        # the default is 20s; slow tools need headroom
            streamable-http:
              connections:
                orders:
                  url: http://orders-mcp.internal:8080
                  endpoint: /mcp        # the default endpoint suffix
            stdio:
              connections:
                local-files:
                  command: npx
                  args:
                    - "-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:

    @Service
    public class SupportAssistant {
    
        private final ChatClient chatClient;
        private final ToolCallbackProvider mcpTools;
    
        public SupportAssistant(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.
    @Component
    public class OrdersOnlyReadTools implements McpToolFilter {
    
        @Override
        public boolean test(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 40
            max-total-tool-calls: 25         # documented default is 150
            on-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) {
    
        ChatOptions options = ToolCallingChatOptions.builder().toolCallbacks(tools).build();
    
        // Turn off the automatic loop for this request only.
        ChatClientResponse response = chatClient.prompt()
                .user(question)
                .options(options)
                .advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
                .call()
                .chatClientResponse();
    
        Prompt prompt = new Prompt(List.of(new UserMessage(question)), options);
        int rounds = 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.";
            }
            ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response.chatResponse());
            prompt = new Prompt(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:

    @Component
    public class ElicitationHandler {
    
        // "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 code
            return answers != null
                    ? new ElicitResult(ElicitResult.Action.ACCEPT, answers)
                    : new ElicitResult(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.

    1. 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.
    2. 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.
    3. Least privilege on the client. Use McpToolFilter allow-lists, and add destructive tools per request only for flows that earned them.
    4. 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.
    @Service
    public class RefundTools {
    
        // 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.
            String caller = 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

    1. 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.
    2. 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.
    3. 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.
    4. 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"]

    For a longer comparison of the protocol with plain function calling, read MCP vs Function Calling. For the in-process side, Tool Calling with Spring AI goes deeper than the previous lesson.

    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.

    1. 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.
    2. Spring Security with JWT validation sits in front of /mcp. request-refund carries @PreAuthorize, and the caller identity is read from the authenticated principal.
    3. 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.
    4. The direct refund tool is deleted from the chatbot. A support agent approves pending refunds in the existing agent desk screen.
    5. 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.

    Official documentation

    Enroll to keep reading

    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.

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    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.

    Join engineers building production AI systems

    Comments