<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Henry Li</title>
    <description>The latest articles on DEV Community by Henry Li (@henry9527).</description>
    <link>https://dev.to/henry9527</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3887743%2F90852950-095f-47a8-948e-2e3169072723.ico</url>
      <title>DEV Community: Henry Li</title>
      <link>https://dev.to/henry9527</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/henry9527"/>
    <language>en</language>
    <item>
      <title>MCP Server &amp; Client in Spring AI: Stop Coupling Tools to Your AI Host</title>
      <dc:creator>Henry Li</dc:creator>
      <pubDate>Sun, 19 Apr 2026 19:27:43 +0000</pubDate>
      <link>https://dev.to/henry9527/mcp-server-client-in-spring-ai-stop-coupling-tools-to-your-ai-host-2l21</link>
      <guid>https://dev.to/henry9527/mcp-server-client-in-spring-ai-stop-coupling-tools-to-your-ai-host-2l21</guid>
      <description>&lt;p&gt;If you've built an LLM feature in Spring Boot, you've probably done something like this: created a &lt;code&gt;@Bean&lt;/code&gt; with &lt;code&gt;@Tool&lt;/code&gt;-annotated methods, wired it into your &lt;code&gt;ChatClient&lt;/code&gt;, and shipped it. That works fine — until your tool set grows, multiple AI applications want to reuse the same tools, or you need to update a tool without redeploying the entire AI service.&lt;/p&gt;

&lt;p&gt;That's the problem MCP (Model Context Protocol) solves. This post walks through a two-service setup I built and verified: a standalone MCP Tool Server and an AI Chat Service that discovers tools dynamically over Streamable HTTP — &lt;strong&gt;no restart required when tools change&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The full solution with runnable code, Docker Compose, and execution evidence is at &lt;a href="https://exesolution.com/solutions/MCP-Server-Client-in-Spring-AI-Dynamic-Tool-Discovery" rel="noopener noreferrer"&gt;exesolution.com&lt;/a&gt;. This post covers the core problem and how to get it running locally.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with In-Process Tool Registration
&lt;/h2&gt;

&lt;p&gt;When you register tools inside the same Spring Boot app that handles LLM interactions, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deployment coupling&lt;/strong&gt; — every new tool means a new deployment of the AI service, even though the AI logic didn't change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No sharing&lt;/strong&gt; — if three different AI applications need the same "get order status" tool, you copy-paste the implementation into each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No trust boundary&lt;/strong&gt; — a bug in a tool method can crash the process that's serving your users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static inventory&lt;/strong&gt; — tools are fixed at startup. Adding one at runtime? Not without a restart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero visibility&lt;/strong&gt; — tool invocations vanish inside the &lt;code&gt;ChatClient&lt;/code&gt; execution loop with no structured logs or traces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The naive fix is "just put everything in one service." But once you have 20 tools across 5 domains, that service becomes the new monolith.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Two Services, One Protocol
&lt;/h2&gt;

&lt;p&gt;The setup has two independently deployable Spring Boot apps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
  └─→ AI Chat Service (:8081)
          └─→ ChatClient (Spring AI)
                  └─→ LLM (gpt-4o-mini)
                  └─→ MCP Client
                          └─→ MCP Tool Server (:8080)  ← POST /mcp
                                  └─→ @McpTool-annotated service methods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;MCP Tool Server&lt;/strong&gt; — owns tool implementations. Exposes them via &lt;code&gt;@McpTool&lt;/code&gt; annotations over Streamable HTTP. Deployed and versioned independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Chat Service&lt;/strong&gt; — user-facing REST API. Knows nothing about specific tools. Uses &lt;code&gt;SyncMcpToolCallbackProvider&lt;/code&gt; to auto-discover whatever tools the server exposes, on every request.&lt;/p&gt;

&lt;p&gt;The key insight: &lt;code&gt;ToolCallbackProvider&lt;/code&gt; re-fetches the tool list from the server on each &lt;code&gt;getToolCallbacks()&lt;/code&gt; call. Add a new &lt;code&gt;@McpTool&lt;/code&gt; bean, hit the refresh endpoint, and the next conversation picks it up — no restart of either service.&lt;/p&gt;




&lt;h2&gt;
  
  
  Defining a Tool: One Annotation
&lt;/h2&gt;

&lt;p&gt;On the server side, any Spring bean method can become an MCP tool with &lt;code&gt;@Tool&lt;/code&gt; (Spring AI's annotation):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderTool&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Tool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Get the current status and details of an order by its ID"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getOrderStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nd"&gt;@ToolParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"The unique order identifier, e.g. ORD-12345"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                        &lt;span class="s"&gt;"orderId"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;           &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                        &lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatus&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                        &lt;span class="s"&gt;"estimatedDelivery"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEstimatedDelivery&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                        &lt;span class="s"&gt;"items"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;             &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getItems&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
                        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalArgumentException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order not found: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spring AI reads the annotation at startup and generates a JSON Schema for the parameters automatically. The LLM receives this schema and knows exactly how to call the tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wiring the Client: One Line
&lt;/h2&gt;

&lt;p&gt;On the AI Host side, wiring all server tools into &lt;code&gt;ChatClient&lt;/code&gt; takes one method call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="nf"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                          &lt;span class="nc"&gt;SyncMcpToolCallbackProvider&lt;/span&gt; &lt;span class="n"&gt;toolCallbackProvider&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultTools&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toolCallbackProvider&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// ← entire server tool registry&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From here, when a user asks "What's the status of order ORD-12345?", the LLM decides to call &lt;code&gt;getOrderStatus&lt;/code&gt;, Spring AI dispatches it over MCP, the tool runs on the server, the result comes back, and the LLM incorporates it into the reply — entirely transparent to the controller layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;MCP Tool Server&lt;/strong&gt; (&lt;code&gt;application.properties&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;spring.ai.mcp.server.name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;tool-server&lt;/span&gt;
&lt;span class="py"&gt;spring.ai.mcp.server.version&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1.0.0&lt;/span&gt;
&lt;span class="py"&gt;spring.ai.mcp.server.protocol&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;STREAMABLE&lt;/span&gt;
&lt;span class="py"&gt;server.port&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AI Chat Service&lt;/strong&gt; (&lt;code&gt;application.properties&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;spring.ai.mcp.client.toolcallback.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;spring.ai.mcp.client.connections.tool-server.url&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;${MCP_SERVER_URL}/mcp&lt;/span&gt;
&lt;span class="py"&gt;spring.ai.mcp.client.connections.tool-server.transport&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;STREAMABLE_HTTP&lt;/span&gt;
&lt;span class="py"&gt;spring.ai.openai.api-key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;${OPENAI_API_KEY}&lt;/span&gt;
&lt;span class="py"&gt;spring.ai.openai.chat.options.model&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;
&lt;span class="py"&gt;server.port&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8081&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dependencies&lt;/strong&gt; — MCP Server (&lt;code&gt;pom.xml&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.ai&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-ai-starter-mcp-server-webmvc&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dependencies&lt;/strong&gt; — AI Host (&lt;code&gt;pom.xml&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.ai&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-ai-starter-mcp-client&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.ai&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-ai-starter-model-openai&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Running It Locally
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Docker Desktop, JDK 17, an OpenAI-compatible API key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Clone and configure&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.template .env
&lt;span class="c"&gt;# add OPENAI_API_KEY=sk-...&lt;/span&gt;

&lt;span class="c"&gt;# 2. Start both services&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verify both services are up:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8080/actuator/health | jq .status
&lt;span class="c"&gt;# → "UP"&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8081/actuator/health | jq .status
&lt;span class="c"&gt;# → "UP"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Confirm the tool registry (admin endpoint):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8080/admin/tools | jq &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="c"&gt;# → list of @McpTool-annotated methods with name, description, inputSchema&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Trigger a tool call through the chat API:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8081/api/chat &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;TOKEN&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"sessionId":"sess-001","message":"What is the status of order ORD-12345?"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="c"&gt;# → {"reply":"Order ORD-12345 is currently SHIPPED...","toolsUsed":["getOrderStatus"]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verify the tool call hit the server:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs mcp-tool-server | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"tools/call"&lt;/span&gt;
&lt;span class="c"&gt;# → log lines showing getOrderStatus invoked with orderId=ORD-12345&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dynamic tool discovery — no restart needed:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add a new tool bean to the server, then:&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/admin/tools/refresh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;ADMIN_TOKEN&amp;gt;"&lt;/span&gt;
&lt;span class="c"&gt;# → {"registered":["getOrderStatus","searchProducts",...]}&lt;/span&gt;

&lt;span class="c"&gt;# Next chat request immediately picks up the new tool&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8081/api/chat &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;TOKEN&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"sessionId":"sess-001","message":"Search for electronics products"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq .reply
&lt;span class="c"&gt;# → uses the newly registered searchProducts tool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What the Stateless Transport Mode Gives You
&lt;/h2&gt;

&lt;p&gt;By default the server runs in stateful &lt;code&gt;STREAMABLE&lt;/code&gt; mode (sessions via &lt;code&gt;Mcp-Session-Id&lt;/code&gt; headers). For horizontally scaled deployments behind a load balancer, switch to stateless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# on mcp-tool-server
&lt;/span&gt;&lt;span class="py"&gt;spring.ai.mcp.server.protocol&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;STATELESS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In stateless mode the server returns &lt;code&gt;application/json&lt;/code&gt; per request. No session affinity required. The same chat requests work identically — the difference is purely at the transport layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's in the Full Solution
&lt;/h2&gt;

&lt;p&gt;This post covers the core problem and the minimal working setup. The complete verified solution at exesolution.com includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full source code for both Spring Boot modules (pom.xml, all Java classes, Docker Compose)&lt;/li&gt;
&lt;li&gt;Three &lt;code&gt;@McpTool&lt;/code&gt; implementations: &lt;code&gt;OrderTool&lt;/code&gt;, &lt;code&gt;ProductTool&lt;/code&gt;, and &lt;code&gt;WeatherTool&lt;/code&gt; (the last one calls &lt;code&gt;open-meteo.com&lt;/code&gt; in real time — verifiable live data)&lt;/li&gt;
&lt;li&gt;Security configuration: &lt;code&gt;/mcp&lt;/code&gt; endpoint internal-only, &lt;code&gt;/api/chat&lt;/code&gt; JWT-protected, &lt;code&gt;/admin/**&lt;/code&gt; role-gated&lt;/li&gt;
&lt;li&gt;Architecture diagram and request flow diagram&lt;/li&gt;
&lt;li&gt;Evidence Pack: 10 verification screenshots from actual execution — health checks, tool registry, chat responses, server-side logs, dynamic refresh&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://exesolution.com/solutions/MCP-Server-Client-in-Spring-AI-Dynamic-Tool-Discovery" rel="noopener noreferrer"&gt;Full solution + runnable code + evidence at exesolution.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Free registration required to access the code bundle and evidence images.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;The pattern here — separate MCP server, auto-discovering client — pays off when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple AI applications need the same tools (deploy once, use everywhere)&lt;/li&gt;
&lt;li&gt;Tool implementations need independent scaling or deployment cadence&lt;/li&gt;
&lt;li&gt;You want a trust boundary between the LLM execution context and the actual side-effecting code&lt;/li&gt;
&lt;li&gt;You're connecting to Claude Desktop, VS Code Copilot, or any other MCP-compatible client — the same server JAR works for all of them without code changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're already using Spring AI for chat and RAG, adding an MCP server is one dependency and a few annotations. The split into two services pays for itself the first time you update a tool without touching the AI host.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have questions about the setup or ran into something unexpected? Drop a comment below.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>java</category>
      <category>ai</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
