<?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: Abiral Khanal</title>
    <description>The latest articles on DEV Community by Abiral Khanal (@abir4l).</description>
    <link>https://dev.to/abir4l</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F62190%2Fe7c0e2f1-32d3-4167-8018-5ae6d8e77d85.jpeg</url>
      <title>DEV Community: Abiral Khanal</title>
      <link>https://dev.to/abir4l</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abir4l"/>
    <language>en</language>
    <item>
      <title>How MCP and tool calling really work: I built an AI agent from scratch in Java and traced every step</title>
      <dc:creator>Abiral Khanal</dc:creator>
      <pubDate>Sat, 31 Oct 2026 18:00:21 +0000</pubDate>
      <link>https://dev.to/abir4l/how-mcp-and-tool-calling-really-work-i-built-an-ai-agent-from-scratch-in-java-and-traced-every-step-5enp</link>
      <guid>https://dev.to/abir4l/how-mcp-and-tool-calling-really-work-i-built-an-ai-agent-from-scratch-in-java-and-traced-every-step-5enp</guid>
      <description>&lt;p&gt;I asked a local AI model to add "buy milk" to my notes file. &lt;strong&gt;It did&lt;/strong&gt;. Then I traced every step to find out what actually happened.&lt;/p&gt;

&lt;p&gt;To do that, I built the whole thing from scratch: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  an MCP server&lt;/li&gt;
&lt;li&gt;  an MCP client and &lt;/li&gt;
&lt;li&gt;  an agent loop in plain Java and Spring Boot,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;no AI frameworks and no MCP SDK, talking to Qwen 2.5 7B running locally through Ollama.&lt;/p&gt;

&lt;p&gt;"From scratch" means no frameworks, not no help: Claude co-authored the code and this article, because writing every line by hand in 2026 would apparently make me a caveman.&lt;/p&gt;

&lt;p&gt;Yes, I know: MCP has been around for about two years. I'm late to the party. But if even one person walks away with a new understanding, that's the best outcome I could ask for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;One plain-text file, &lt;code&gt;data/db&lt;/code&gt;, holds my notes. The &lt;strong&gt;MCP server&lt;/strong&gt; is the only thing that touches it, through five tools: &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt; (replace everything), &lt;code&gt;append&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt; (find and replace) and &lt;code&gt;clear&lt;/code&gt;. A simple File I/O method, &lt;a href="https://github.com/abir4l/mcp_server/blob/master/src/main/java/dev/abiral/mcp/mcp_server/tool/ToolBox.java" rel="noopener noreferrer"&gt;ref&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You ask the &lt;strong&gt;host&lt;/strong&gt; in plain English ("Add buy milk"), it asks the model which tool fits, and runs it on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; with Compose: runs the MCP server and the host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama&lt;/strong&gt;, installed natively (&lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;ollama.com&lt;/a&gt;), not in Docker, so the model can use your GPU. On Linux, set &lt;code&gt;OLLAMA_HOST=0.0.0.0&lt;/code&gt; so the containers can reach it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model&lt;/strong&gt;: &lt;code&gt;ollama pull qwen2.5:7b&lt;/code&gt; (about 4.7 GB; 8 GB RAM minimum, 16 GB comfortable).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git&lt;/strong&gt;. No Java needed unless you run outside Docker.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/abir4l/mcp_server
git clone https://github.com/abir4l/local_agent
ollama pull qwen2.5:7b          &lt;span class="c"&gt;# with Ollama running&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;local_agent &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The map
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhmml3zwnsnfhgnoa3p2w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhmml3zwnsnfhgnoa3p2w.png" alt="Architecture: the host sits in the middle and talks to both Ollama and the MCP server, which never talk to each other" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;local_agent&lt;/strong&gt; (port 8081) is the &lt;em&gt;host&lt;/em&gt;. It runs the agent loop, keeps the conversation and makes every network call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama&lt;/strong&gt; (port 11434) runs the model. Text in, text out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mcp_server&lt;/strong&gt; (port 8080) owns the tools. It's the only thing that touches &lt;code&gt;data/db&lt;/code&gt;, the notes file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Part 1: The layers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The model is just numbers
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ollama pull qwen2.5:7b&lt;/code&gt; downloads about 4.7 GB of weights: billions of numbers. Together with a function that predicts the next token, that's the whole model. It doesn't listen on a port, doesn't speak JSON or HTTP, and does nothing on its own. A file of numbers can't write to your disk. Something else has to.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Ollama is a database server, but for models
&lt;/h3&gt;

&lt;p&gt;I used to think Ollama, ChatGPT and Claude were abstractions over the model, so my own app would skip them and talk to the model directly. But &lt;strong&gt;the model is just numbers, and an app can't talk to numbers&lt;/strong&gt;. Something has to load it and give you an interface to send text and get text back: a model server. &lt;/p&gt;

&lt;p&gt;Locally that's Ollama; in the cloud it's the OpenAI or Anthropic API. That's the layer you can't bypass. &lt;/p&gt;

&lt;p&gt;ChatGPT and the Claude app are one level higher: they're &lt;em&gt;hosts&lt;/em&gt; like my &lt;code&gt;local_agent&lt;/code&gt;, with a UI, history and tools on top of that API. Calling the OpenAI API isn't calling ChatGPT.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Local&lt;/th&gt;
&lt;th&gt;OpenAI&lt;/th&gt;
&lt;th&gt;Anthropic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;qwen2.5:7b&lt;/code&gt; on your disk&lt;/td&gt;
&lt;td&gt;GPT (private weights)&lt;/td&gt;
&lt;td&gt;Claude (private weights)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model server (API)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ollama&lt;/td&gt;
&lt;td&gt;OpenAI API&lt;/td&gt;
&lt;td&gt;Anthropic API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Host / app&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;local_agent&lt;/code&gt;, or Ollama's own chat app&lt;/td&gt;
&lt;td&gt;ChatGPT&lt;/td&gt;
&lt;td&gt;Claude app&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;ChatGPT is to the OpenAI API what my &lt;code&gt;local_agent&lt;/code&gt; is to Ollama. Build on the API and &lt;em&gt;you&lt;/em&gt; write the ChatGPT part: history, tools, the loop.&lt;/p&gt;

&lt;p&gt;One twist: locally, Ollama sits in &lt;strong&gt;two rows at once&lt;/strong&gt;. Its server (the API on port 11434) is the model-server layer, but its desktop chat window and &lt;code&gt;ollama run&lt;/code&gt; in the terminal are hosts, thin clients of that same API. Imagine OpenAI shipping the API and ChatGPT in one download. My &lt;code&gt;local_agent&lt;/code&gt; skips Ollama's chat window and talks to the API directly, the same way your own app would skip ChatGPT and call the OpenAI API.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. There is no standard for talking to models (MCP vs function calling)
&lt;/h3&gt;

&lt;p&gt;Every vendor has its own JSON dialect. OpenAI returns tool arguments as a JSON &lt;em&gt;string&lt;/em&gt;, Ollama as a JSON &lt;em&gt;object&lt;/em&gt;, and Anthropic uses &lt;code&gt;tool_use&lt;/code&gt; content blocks instead of &lt;code&gt;tool_calls&lt;/code&gt;. Same idea, three shapes, so every host needs an adapter.&lt;/p&gt;

&lt;p&gt;That's also the answer to "MCP vs function calling". &lt;strong&gt;Function calling&lt;/strong&gt; is the model side: you describe tools to the model and it replies with a request to use one. &lt;strong&gt;MCP standardized the tool side, not the model side&lt;/strong&gt;: how a host finds and calls tools on a separate server. A real agent uses both. Since MCP's &lt;code&gt;inputSchema&lt;/code&gt; and Ollama's &lt;code&gt;parameters&lt;/code&gt; are both plain JSON Schema, my adapter between them is a single &lt;code&gt;map()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: The illusions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4. Are LLMs stateless? Yes. Chat is an illusion
&lt;/h3&gt;

&lt;p&gt;Maybe you already know this one: the model remembers nothing between calls. The host resends the entire conversation every turn, so "memory" is just a list in my Java process. That's all it ever is.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. How tool calling actually works: it's just text
&lt;/h3&gt;

&lt;p&gt;There is no special channel. Ollama's chat template renders your JSON tools &lt;em&gt;into the prompt&lt;/em&gt; (see it with &lt;code&gt;ollama show qwen2.5:7b --template&lt;/code&gt;). Trimmed, the model sees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;|im_start|&amp;gt;system
# Tools
You are provided with function signatures within &amp;lt;tools&amp;gt;&amp;lt;/tools&amp;gt; XML tags:
&amp;lt;tools&amp;gt;
{"type": "function", "function": {"name": "append", "description": "Append content to the end of the file", ...}}
...
&amp;lt;/tools&amp;gt;
For each function call, return a json object with function name and arguments within &amp;lt;tool_call&amp;gt;&amp;lt;/tool_call&amp;gt; XML tags
&amp;lt;|im_end|&amp;gt;
&amp;lt;|im_start|&amp;gt;user
Add buy milk to my notes&amp;lt;|im_end|&amp;gt;
&amp;lt;|im_start|&amp;gt;assistant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model predicts the most likely next tokens, which, because it was trained on this format, are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;tool_call&amp;gt;
{"name": "append", "arguments": {"content": "buy milk"}}
&amp;lt;/tool_call&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ollama parses that text back into a neat &lt;code&gt;tool_calls&lt;/code&gt; JSON field. The "function call" is a string that looks like one. (Start Ollama with &lt;code&gt;OLLAMA_DEBUG=1&lt;/code&gt; to see the full rendered prompt.)&lt;/p&gt;

&lt;h3&gt;
  
  
  6. The model never touches the file
&lt;/h3&gt;

&lt;p&gt;Ollama returns that &lt;code&gt;tool_calls&lt;/code&gt; JSON to my host, and it's done. It has no idea an MCP server exists. My host reads the request, calls the MCP server over HTTP, and pastes the result back into the conversation as a &lt;code&gt;role: "tool"&lt;/code&gt; message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ollama and the MCP server never talk to each other.&lt;/strong&gt; The model &lt;em&gt;requests&lt;/em&gt;. The host &lt;em&gt;decides&lt;/em&gt; and &lt;em&gt;acts&lt;/em&gt;. That's why the host is the real control point for safety, not the model.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to add your own data to an LLM (no training required)
&lt;/h3&gt;

&lt;p&gt;This is what I actually set out to learn. When the model calls &lt;code&gt;read&lt;/code&gt;, my notes come back as a &lt;code&gt;role: "tool"&lt;/code&gt; message and sit in the conversation like any other text. The model "knows" my file because the host pasted it into the prompt.&lt;/p&gt;

&lt;p&gt;Every popular way to give an LLM your own data is this trick, except one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt stuffing&lt;/strong&gt;: paste the data in yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool calling / MCP&lt;/strong&gt;: the model asks, the host fetches (this project).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG&lt;/strong&gt;: the host searches your documents and pastes in the relevant chunks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-tuning&lt;/strong&gt;: changes the weights. Good for behaviour and style, usually wrong for "make it know my documents".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So "train ChatGPT on my own data" usually means one of the first three. Choosing what goes into the context window is &lt;strong&gt;context engineering&lt;/strong&gt;, and RAG exposed as an MCP tool is agentic RAG.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: MCP on the wire
&lt;/h2&gt;

&lt;h3&gt;
  
  
  7. MCP is JSON-RPC on one endpoint, not REST
&lt;/h3&gt;

&lt;p&gt;An MCP server has exactly one endpoint, &lt;code&gt;POST /mcp&lt;/code&gt;, and the operation lives in the body's &lt;code&gt;method&lt;/code&gt; field (JSON-RPC 2.0). Since the 2026-07-28 spec, MCP is also stateless: no &lt;code&gt;initialize&lt;/code&gt; handshake and no session ID. Every request carries its protocol version in &lt;code&gt;params._meta&lt;/code&gt;, and the method and tool name are copied into headers, so a gateway can route without parsing the body.&lt;/p&gt;

&lt;h4&gt;
  
  
  What an MCP server's API looks like
&lt;/h4&gt;

&lt;p&gt;If you build REST APIs for a living, this is the light-bulb view. Here is my server's entire API, OpenAPI-style:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /mcp                         the only endpoint (GET, DELETE -&amp;gt; 405)

method            params              returns
server/discover   (none)              supported versions, capabilities
tools/list        (none)              [{ name, description, inputSchema }]
tools/call        name, arguments     { content: [...], isError }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the API the model actually sees, as returned by &lt;code&gt;tools/list&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tool      arguments          what it does
read      (none)             return the whole file
write     content            replace the whole file
append    content            add a line at the end
update    search, replace    find and replace text
clear     (none)             empty the file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing: two lists. Think of &lt;code&gt;tools/list&lt;/code&gt; as an OpenAPI doc written for a model instead of a developer. The full spec adds the same list-and-read pattern for resources (&lt;code&gt;resources/list&lt;/code&gt;, &lt;code&gt;resources/read&lt;/code&gt;) and prompts (&lt;code&gt;prompts/list&lt;/code&gt;, &lt;code&gt;prompts/get&lt;/code&gt;), but tools are where the action is. One call on the wire looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /mcp
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: append

{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"append","arguments":{"content":"buy milk"},
 "_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  MCP vs API: isn't MCP just an API?
&lt;/h4&gt;

&lt;p&gt;Technically, yes. The difference is who it's designed for. A normal API is designed for a developer, who reads the docs and writes a custom client. An MCP server describes itself to a model: &lt;code&gt;tools/list&lt;/code&gt; returns every tool with a plain-English description and a JSON Schema, so any MCP host can use any MCP server with no custom code. MCP usually wraps your existing APIs rather than replacing them.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. There are two kinds of errors
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protocol errors&lt;/strong&gt; (bad JSON, wrong headers, unknown tool) come back as a JSON-RPC &lt;code&gt;error&lt;/code&gt;. They're client bugs, for the developer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool errors&lt;/strong&gt; (missing argument, wrong type) come back as a normal &lt;code&gt;result&lt;/code&gt; with &lt;code&gt;isError: true&lt;/code&gt;. They're for the &lt;strong&gt;model&lt;/strong&gt;, so it can fix its call and retry.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// unknown tool is a protocol error, bad arguments are a tool error the model can fix&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toolName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolNotFoundException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nc"&gt;RpcException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invalidParams&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unknown tool: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;toolName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HttpStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;OK&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolArgumentException&lt;/span&gt; &lt;span class="n"&gt;e&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;CallToolResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;resultMeta&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;Small models make small mistakes. Hand back &lt;code&gt;Param (content) is required&lt;/code&gt; instead of throwing, and they usually fix it on the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 4: The full trace
&lt;/h2&gt;

&lt;p&gt;One &lt;code&gt;/agent&lt;/code&gt; request, every hop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. You → host&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; localhost:8081/agent &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"message":"Add buy milk to my notes"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Host → MCP server:&lt;/strong&gt; &lt;code&gt;tools/list&lt;/code&gt; (id 1) returns the five tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Host → Ollama&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/api/chat&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qwen2.5:7b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Add buy milk to my notes"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"append"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Append content to the end of the file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Content that gets appended"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;write&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;clear&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stream"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keep_alive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30m"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Ollama → host:&lt;/strong&gt; the model wants a tool, and says nothing else (&lt;code&gt;content&lt;/code&gt; is empty).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"append"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"buy milk"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Host → MCP server:&lt;/strong&gt; the &lt;code&gt;tools/call&lt;/code&gt; request shown in moment 7. This is the only moment anything touches the disk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"resultType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"complete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Success"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="nl"&gt;"isError"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"_meta"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"io.modelcontextprotocol/serverInfo"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"mcp_server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"0.0.1-SNAPSHOT"&lt;/span&gt;&lt;span class="p"&gt;}}}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Host → Ollama again, with the whole history&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Add buy milk to my notes"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"append"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"buy milk"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"append"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. Ollama → host:&lt;/strong&gt; &lt;code&gt;"content": "I've added \"buy milk\" to your notes."&lt;/code&gt; with no &lt;code&gt;tool_calls&lt;/code&gt;, so the loop stops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Host → you:&lt;/strong&gt; the reply, plus a list of every tool call made.&lt;/p&gt;

&lt;p&gt;Seven HTTP calls between three processes for two words in a text file. The model was in two of them, receiving text and returning text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 5: The harness
&lt;/h2&gt;

&lt;h3&gt;
  
  
  9. The agent harness matters more than people think
&lt;/h3&gt;

&lt;p&gt;Everything around the model is the &lt;strong&gt;agent harness&lt;/strong&gt;. Here, that's my host. The loop itself is tiny (logging trimmed):&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="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxSteps&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Message&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollamaClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// the tool_calls must stay in history&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasToolCalls&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="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;invocations&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolCall&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toolCalls&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;ToolInvocation&lt;/span&gt; &lt;span class="n"&gt;invocation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;invoke&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="n"&gt;invocations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invocation&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toolResult&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;invocation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;output&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Agent did not finish within "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxSteps&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" steps"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The behaviour lives in the details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An iteration guard&lt;/strong&gt; (&lt;code&gt;agent.max-steps=5&lt;/code&gt;), because a model that calls tools forever is a real failure mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keeping the model's own tool call in history&lt;/strong&gt;, or the tool result makes no sense to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool descriptions are written for the model.&lt;/strong&gt; My &lt;code&gt;write&lt;/code&gt; tool says "Write all contents to file". To a 7B model asked to "add" something, that can look fine, and then your notes are gone. "Replaces the entire file. To add a note, use append." works better.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 7B model is slow on a laptop and not deterministic, but it's good enough to learn on, because every failure is visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;h3&gt;
  
  
  10. Frameworks hide all of this
&lt;/h3&gt;

&lt;p&gt;Spring AI can do everything above with an annotation like &lt;code&gt;@McpTool&lt;/code&gt;. I'd use it in production. But now that Claude and I built it without one, I know what it hides, and when something breaks I know which of the seven hops to check.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;v2&lt;/strong&gt;: the same project with Spring AI, compared line by line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v3&lt;/strong&gt;: multiple notes files, then agentic RAG with a search tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need to train an LLM on my own data?&lt;/strong&gt; Usually not. Put the data in the context window with a prompt, a tool/MCP server or RAG. Fine-tuning changes behaviour, not knowledge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP vs RAG?&lt;/strong&gt; RAG retrieves relevant chunks into the prompt. MCP lets the model call tools, and search can be one. Combined, that's agentic RAG.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the model execute the tool?&lt;/strong&gt; No. It writes text asking for one. The host runs the tool and sends back the result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is MCP the same as function calling?&lt;/strong&gt; No. Function calling is how a model asks for a tool. MCP is how a host finds and calls tools on another server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need MCP for tool calling?&lt;/strong&gt; No. You can define tools in your app directly. MCP pays off when tools are shared across apps or teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a local model use MCP tools?&lt;/strong&gt; Yes, if it was trained for tool calling (Qwen 2.5, Llama 3.1, Qwen 3). The model never talks to the MCP server anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changed in MCP 2026-07-28?&lt;/strong&gt; It's stateless: no &lt;code&gt;initialize&lt;/code&gt;, no sessions, and context travels in &lt;code&gt;params._meta&lt;/code&gt; plus mirrored headers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;Clone both repos, run &lt;code&gt;docker compose up --build&lt;/code&gt; in &lt;code&gt;local_agent&lt;/code&gt;, and send the &lt;code&gt;curl&lt;/code&gt; from hop 1. Ollama runs natively on your machine, and Docker runs the rest.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/abir4l/mcp_server" rel="noopener noreferrer"&gt;github.com/abir4l/mcp_server&lt;/a&gt; (includes a &lt;a href="https://github.com/abir4l/mcp_server/blob/master/docs/mcp-server-guide.md" rel="noopener noreferrer"&gt;full client integration guide&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/abir4l/local_agent" rel="noopener noreferrer"&gt;github.com/abir4l/local_agent&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model is just numbers. Everything else is software, and software is something we already know how to read.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Co-written with Claude. If this was useful, a star on the repo helps.&lt;/em&gt;&lt;/p&gt;

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