<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Streaming UI on Sven Ruppert</title><link>https://svenruppert.com/tags/streaming-ui/</link><description>Sven Ruppert — Java Veteran, Speaker, Trainer &amp; Bushcrafter. Articles, talks, workshops and videos on Core Java, Cybersecurity, Vaadin and Developer Relations.</description><generator>Hugo</generator><language>en</language><managingEditor>sven.ruppert@gmail.com (Sven Ruppert)</managingEditor><webMaster>sven.ruppert@gmail.com (Sven Ruppert)</webMaster><copyright>© 2026 Sven Ruppert</copyright><atom:link href="https://svenruppert.com/tags/streaming-ui/index.xml" rel="self" type="application/rss+xml"/><image><url>https://svenruppert.com/img/sven-ruppert.jpg</url><title>Sven Ruppert</title><link>https://svenruppert.com/tags/streaming-ui/</link></image><lastBuildDate>Fri, 26 Jun 2026 12:00:00 +0200</lastBuildDate><item><title>The Vaadin UI of the Recipe Assistant — Part 1: The Main View</title><link>https://svenruppert.com/posts/recipe-assistant-vaadin-ui-part-1-the-main-view/</link><pubDate>Fri, 26 Jun 2026 12:00:00 +0200</pubDate><author>sven.ruppert@gmail.com (Sven Ruppert)</author><dc:creator>Sven Ruppert</dc:creator><guid isPermaLink="true">https://svenruppert.com/posts/recipe-assistant-vaadin-ui-part-1-the-main-view/</guid><description>Building a Vaadin Flow interface that puts two model answers side by side — with and without an MCP server attached — to make the value of the protocol immediately perceivable. A tour through six concrete challenges, from running Vaadin without a starter to keeping state consistent under two writers.</description><content:encoded>&lt;![CDATA[<h2 id="1-introduction-the-interface-as-proof">1. Introduction: The Interface as Proof</h2><p>The preceding article in this series built an MCP server using nothing but the
facilities of the JDK and placed the Model Context Protocol in its conceptual
context. Its outcome, however, was not the server itself but a single image: two
answers from the same language model, set side by side, on the left without an
attached server and on the right with one. The left-hand answer draws solely on
the model&rsquo;s general knowledge of the world and proposes recipes that bear no
relation to the user&rsquo;s actual circumstances. The right-hand answer reaches, by
way of the MCP server, into the actual pantry and the stored dietary profile, and
recommends what can be prepared from the available ingredients while respecting
the diet and the medical constraints.</p><p>This image is more than an illustration. It is the actual argument of the entire
project. The value of a protocol for tool integration may be disputed at length
so long as it is merely asserted; yet once two visibly different answers come into
being before the viewer&rsquo;s eyes at the very same moment, the dispute dissolves. The
juxtaposition replaces assertion with immediate perception. For precisely this
reason, it is not only the server but also the interface that produces this image
which merits an examination of its own. Where the first article dealt with the
mechanics of the protocol, the present part descends to the question of its
perceptibility.</p><p>However plain the two-column arrangement may appear to the viewer, its production
is anything but self-evident. For the compelling image to arise, two requests to
the same model must be conducted concurrently, the one without and the other with
an attached tool catalogue. Their answers arrive character by character and at
differing speeds, and they must flow simultaneously into two separate areas
without intermingling. The protocol traffic that makes the right-hand answer
possible in the first place runs hidden during normal operation and must be
rendered legible by deliberate design. And all of this has to happen without the
interface stalling or losing the impression of simultaneity on which its effect
depends.</p><p>From this follows the guiding observation of this part: the persuasive force of
the juxtaposition rests less on the logic of the answers than on the discipline
of their presentation. An interface for model-driven tool use must not merely
display results; it must keep the process of their emergence comprehensible.</p><p>The present part is therefore conceived as a tour through the concrete challenges
rather than as an exhaustive catalogue of every constituent of the interface. Each
of the following chapters takes up a challenge of its own: running Vaadin without
the comfort of a framework, conducting two requests concurrently, channelling a
character-by-character stream into a server-side component tree, drawing an honest
distinction between an unfinished and a completed answer, making the protocol
visible, and finally preserving a consistent state. The beginning is made by the
question of how the application comes to run at all — and that without Spring,
without Jakarta EE, and with as few dependencies as possible.</p><h2 id="2-running-vaadin-without-a-starter">2. Running Vaadin Without a Starter</h2><p>Anyone setting up a Vaadin application usually reaches for a starter. A single
dependency entry, an annotation, and the interface is up and running; the servlet
is registered, the initialisers are discovered, the static resources are mounted,
and the WebSocket channel stands ready. All of this happens behind a façade that
the developer neither sees nor needs to understand. For a project that has made a
deliberate point of forgoing Spring and Jakarta EE, however, this very façade is
the object of interest. The present chapter opens it and shows the few, clearly
nameable steps of which the start-up in fact consists.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-ch02.png" alt="Comparison: with a starter (hidden) versus without a starter (explicit)" loading="lazy" decoding="async"/><p>The entry point could hardly be plainer. The module&rsquo;s<code>Main</code> class first sets the
default locale of the virtual machine to English and then starts the embedded
server on the centrally held port.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kn">package</span><span class="w"/><span class="nn">com.svenruppert.mcp.ui</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"/><span class="nn">com.svenruppert.dependencies.core.logger.HasLogger</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"/><span class="nn">com.svenruppert.mcp.common.Ports</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"/><span class="nn">java.util.Locale</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="kd">public</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="kd">class</span><span class="nc">Main</span><span class="w"/><span class="kd">implements</span><span class="w"/><span class="n">HasLogger</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">main</span><span class="p">(</span><span class="n">String</span><span class="o">[]</span><span class="w"/><span class="n">args</span><span class="p">)</span><span class="w"/><span class="kd">throws</span><span class="w"/><span class="n">Exception</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Locale</span><span class="p">.</span><span class="na">setDefault</span><span class="p">(</span><span class="n">Locale</span><span class="p">.</span><span class="na">ENGLISH</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Main</span><span class="p">().</span><span class="na">run</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">run</span><span class="p">()</span><span class="w"/><span class="kd">throws</span><span class="w"/><span class="n">Exception</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">logger</span><span class="p">().</span><span class="na">info</span><span class="p">(</span><span class="s">"vaadin-ui starting on port {}"</span><span class="p">,</span><span class="w"/><span class="n">Ports</span><span class="p">.</span><span class="na">VAADIN_UI</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">new</span><span class="w"/><span class="n">UiServer</span><span class="p">(</span><span class="n">Ports</span><span class="p">.</span><span class="na">VAADIN_UI</span><span class="p">).</span><span class="na">start</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>Setting the locale is no incidental detail. The entire application presents itself
in English, right down to the<code>lang</code> attribute that Vaadin writes into the
delivered HTML. Without this fixing, the application would adopt the locale of the
host machine and would unexpectedly speak German on a German system. The actual
work is carried out by<code>UiServer</code>, whose start-up routine structures the remainder
of the chapter.</p><p>The first step erects the server and the servlet context. Here the first
non-obvious decision already arises.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">start</span><span class="p">()</span><span class="w"/><span class="kd">throws</span><span class="w"/><span class="n">Exception</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">server</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Server</span><span class="p">(</span><span class="n">port</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ServletContextHandler</span><span class="w"/><span class="n">context</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ServletContextHandler</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">setContextPath</span><span class="p">(</span><span class="s">"/"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">setSessionHandler</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">SessionHandler</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// Sharing the parent classloader avoids the server/system class isolation</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// that a WebAppContext would impose.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">setClassLoader</span><span class="p">(</span><span class="n">Thread</span><span class="p">.</span><span class="na">currentThread</span><span class="p">().</span><span class="na">getContextClassLoader</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// Expose every META-INF/resources/ directory from the classpath as a</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// virtual webapp root. Servlet 3.0 lets JARs ship static assets there</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// (Vaadin's flow-push delivers vaadinPush-min.js under</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// META-INF/resources/VAADIN/static/push/), but ServletContextHandler</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// does not auto-mount them the way WebAppContext does.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">setBaseResource</span><span class="p">(</span><span class="n">metaInfResources</span><span class="p">(</span><span class="n">context</span><span class="p">));</span></span></span></code></pre></div></div><p>A plain<code>ServletContextHandler</code> is used, not the more obvious<code>WebAppContext</code>. The
difference is decisive here. A<code>WebAppContext</code> installs a class loader of its own
whose separation between server and application classes hides the classes from<code>org.eclipse.jetty.*</code> from the application. Vaadin, however, requires these
classes during its own initialisation, which is why a<code>WebAppContext</code> would cause
the Vaadin initialisers to fail. The<code>ServletContextHandler</code> instead shares the
class loader of the calling thread, and the isolation falls away. This very
plainness, however, buys extra effort elsewhere: static resources that a<code>WebAppContext</code> would mount of its own accord must now be provided explicitly. The
helper method responsible for this is examined further below.</p><p>In the second step, the<code>VaadinServlet</code> is registered.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="c1">// Register VaadinServlet at "/*". Async + WebSocket support is needed for @Push.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ServletHolder</span><span class="w"/><span class="n">vaadinHolder</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ServletHolder</span><span class="p">(</span><span class="s">"vaadin"</span><span class="p">,</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">VaadinServlet</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">vaadinHolder</span><span class="p">.</span><span class="na">setInitParameter</span><span class="p">(</span><span class="s">"productionMode"</span><span class="p">,</span><span class="w"/><span class="s">"true"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">vaadinHolder</span><span class="p">.</span><span class="na">setAsyncSupported</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">vaadinHolder</span><span class="p">.</span><span class="na">setInitOrder</span><span class="p">(</span><span class="n">1</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addServlet</span><span class="p">(</span><span class="n">vaadinHolder</span><span class="p">,</span><span class="w"/><span class="s">"/*"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// ServletDeployer is a context listener. With VaadinServlet already registered,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// it logs and skips creation.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addEventListener</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ServletDeployer</span><span class="p">());</span></span></span></code></pre></div></div><p>The servlet is registered under the path pattern<code>/*</code> and thus answers every
request. Three settings deserve attention. Production mode switches off the
development regime with its continuous frontend build and presupposes the
previously produced production bundle. Enabling asynchronous processing is the
precondition for<code>@Push</code> to work at all later on. The init order, finally, ensures
that the servlet comes up early in the initialisation of the context. The<code>ServletDeployer</code> registered afterwards is a context listener that would otherwise
create the servlet itself; since the servlet has already been registered by hand,
it confines itself to a log entry.</p><p>The third and most extensive step constitutes the heart of the framework-free
start-up. Since Vaadin 25, the framework no longer registers its initialisers of
its own accord; they must therefore be registered by hand, and in the correct
order.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="c1">// Vaadin SCIs in correct order. LookupServletContainerInitializer must run first.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addServletContainerInitializer</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ServletContainerInitializerHolder</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">LookupServletContainerInitializer</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">LookupInitializer</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">loadVaadinClass</span><span class="p">(</span><span class="s">"com.vaadin.flow.di.LookupInitializer$ResourceProviderImpl"</span><span class="p">),</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">loadVaadinClass</span><span class="p">(</span><span class="s">"com.vaadin.flow.di.LookupInitializer$StaticFileHandlerFactoryImpl"</span><span class="p">),</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">loadVaadinClass</span><span class="p">(</span><span class="s">"com.vaadin.flow.di.LookupInitializer$AppShellPredicateImpl"</span><span class="p">),</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">DefaultApplicationConfigurationFactory</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">DefaultRoutePathProvider</span><span class="p">.</span><span class="na">class</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addServletContainerInitializer</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ServletContainerInitializerHolder</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">RouteRegistryInitializer</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">MainView</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughView</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">MainLayout</span><span class="p">.</span><span class="na">class</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addServletContainerInitializer</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ServletContainerInitializerHolder</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">VaadinAppShellInitializer</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AppShell</span><span class="p">.</span><span class="na">class</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addServletContainerInitializer</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ServletContainerInitializerHolder</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AnnotationValidator</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AppShell</span><span class="p">.</span><span class="na">class</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addServletContainerInitializer</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ServletContainerInitializerHolder</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ErrorNavigationTargetInitializer</span><span class="p">.</span><span class="na">class</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addServletContainerInitializer</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ServletContainerInitializerHolder</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WebComponentExporterAwareValidator</span><span class="p">.</span><span class="na">class</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addServletContainerInitializer</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ServletContainerInitializerHolder</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WebComponentConfigurationRegistryInitializer</span><span class="p">.</span><span class="na">class</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">));</span></span></span></code></pre></div></div><p>Each of these seven calls registers a<code>ServletContainerInitializer</code> and at the
same time names the classes for which it is responsible. These classes correspond
to the<code>@HandlesTypes</code> that a servlet container would otherwise determine by
scanning the classpath; since registration is done by hand here, they are passed
explicitly. The first initialiser is the service lookup and must run first, as all
the others build upon it. The second sets up the route registry and is given the
three navigable classes<code>MainView</code>,<code>WalkthroughView</code> and<code>MainLayout</code>; only thus
does the route registry find the two views and the shared layout. The remaining
five set up the application shell, validate annotations, determine the error
targets, and validate or register any web components.</p><p>Striking are the three classes loaded via<code>loadVaadinClass</code> in the first call.
These are internal, non-exported Vaadin classes that cannot be written directly as
a class literal. The helper method therefore loads them by name and fails with a
clear message should Vaadin&rsquo;s internal structure ever change.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">Class</span><span class="o">&lt;?&gt;</span><span class="w"/><span class="n">loadVaadinClass</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">name</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">Class</span><span class="p">.</span><span class="na">forName</span><span class="p">(</span><span class="n">name</span><span class="p">,</span><span class="w"/><span class="kc">false</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Thread</span><span class="p">.</span><span class="na">currentThread</span><span class="p">().</span><span class="na">getContextClassLoader</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">ClassNotFoundException</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">throw</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">IllegalStateException</span><span class="p">(</span><span class="s">"Vaadin runtime class not found: "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">name</span><span class="p">,</span><span class="w"/><span class="n">e</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>The fourth step completes the wiring and starts the server. Beforehand, Jakarta
WebSocket support is configured, without which the server push over the WebSocket
channel would not come about.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="c1">// Jakarta WebSocket support so @Push can use the websocket transport.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">JakartaWebSocketServletContainerInitializer</span><span class="p">.</span><span class="na">configure</span><span class="p">(</span><span class="n">context</span><span class="p">,</span><span class="w"/><span class="kc">null</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">server</span><span class="p">.</span><span class="na">setHandler</span><span class="p">(</span><span class="n">context</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Runtime</span><span class="p">.</span><span class="na">getRuntime</span><span class="p">().</span><span class="na">addShutdownHook</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">Thread</span><span class="p">(</span><span class="k">this</span><span class="p">::</span><span class="n">safeStop</span><span class="p">,</span><span class="w"/><span class="s">"ui-shutdown"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">server</span><span class="p">.</span><span class="na">start</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">logger</span><span class="p">().</span><span class="na">info</span><span class="p">(</span><span class="s">"Vaadin UI listening on http://localhost:{}/"</span><span class="p">,</span><span class="w"/><span class="n">port</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">CountDownLatch</span><span class="w"/><span class="n">latch</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">CountDownLatch</span><span class="p">(</span><span class="n">1</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Runtime</span><span class="p">.</span><span class="na">getRuntime</span><span class="p">().</span><span class="na">addShutdownHook</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">Thread</span><span class="p">(</span><span class="n">latch</span><span class="p">::</span><span class="n">countDown</span><span class="p">,</span><span class="w"/><span class="s">"ui-latch"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">latch</span><span class="p">.</span><span class="na">await</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>After the context has been set as the handler, a shutdown hook is installed that
brings the server to an orderly halt on interruption. The actual start then
reports the reachable address. Since the main thread would otherwise end and the
virtual machine would terminate the application, a<code>CountDownLatch</code> keeps the
thread open until a second shutdown hook releases it. The server thus runs until
it is explicitly stopped.</p><p>There remains the helper method that provides the static resources — the very work
that a<code>WebAppContext</code> would take on of its own accord.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">Resource</span><span class="w"/><span class="nf">metaInfResources</span><span class="p">(</span><span class="n">ServletContextHandler</span><span class="w"/><span class="n">context</span><span class="p">)</span><span class="w"/><span class="kd">throws</span><span class="w"/><span class="n">Exception</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ClassLoader</span><span class="w"/><span class="n">cl</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">Thread</span><span class="p">.</span><span class="na">currentThread</span><span class="p">().</span><span class="na">getContextClassLoader</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ResourceFactory</span><span class="w"/><span class="n">factory</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">ResourceFactory</span><span class="p">.</span><span class="na">of</span><span class="p">(</span><span class="n">context</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// Servlet 3 convention: each JAR may carry static files under META-INF/resources/.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// flow-push delivers vaadinPush-min.js under META-INF/resources/VAADIN/static/push/,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// and Vaadin's prebuilt production bundle ships under</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// META-INF/resources/VAADIN/build/ in vaadin-prod-bundle.jar — both are</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// exposed by combining every classpath entry's META-INF/resources tree.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">List</span><span class="o">&lt;</span><span class="n">Resource</span><span class="o">&gt;</span><span class="w"/><span class="n">resources</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ArrayList</span><span class="o">&lt;&gt;</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">URL</span><span class="w"/><span class="n">u</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">Collections</span><span class="p">.</span><span class="na">list</span><span class="p">(</span><span class="n">cl</span><span class="p">.</span><span class="na">getResources</span><span class="p">(</span><span class="s">"META-INF/resources"</span><span class="p">)))</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">resources</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">factory</span><span class="p">.</span><span class="na">newResource</span><span class="p">(</span><span class="n">u</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">resources</span><span class="p">.</span><span class="na">isEmpty</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">factory</span><span class="p">.</span><span class="na">newResource</span><span class="p">(</span><span class="s">"."</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">resources</span><span class="p">.</span><span class="na">size</span><span class="p">()</span><span class="w"/><span class="o">==</span><span class="w"/><span class="n">1</span><span class="w"/><span class="o">?</span><span class="w"/><span class="n">resources</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="n">0</span><span class="p">)</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">ResourceFactory</span><span class="p">.</span><span class="na">combine</span><span class="p">(</span><span class="n">resources</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>By the Servlet 3 convention, every archive on the classpath may carry static
files under<code>META-INF/resources</code>. Vaadin makes use of this in several places, for
instance for the push script and for the prebuilt production bundle. The method
gathers all such directories from the classpath and combines them into a single
virtual root, so that the server delivers them as one resource collection.</p><p>The entire sequence can be grasped at a glance.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-diagram-01.png" alt="Framework-free Vaadin start-up flow" loading="lazy" decoding="async"/><p>With that, the framework-free start-up is complete. It consists of erecting the
context, providing the static resources, registering the servlet, explicitly
registering the seven initialisers, configuring the WebSocket, and the actual
start. Each of these steps is visible and traceable in the source. What a starter
conceals as a convenience lies open here — and this very openness is the reward of
forgoing it. The price is a handful of additional lines; the return is a start-up
that can be understood entirely with the platform&rsquo;s own means, without the detour
through a further layer of abstraction. It is on this foundation that the next
chapter builds, turning to the construction of the main view itself.</p><h2 id="3-conducting-two-requests-side-by-side">3. Conducting Two Requests Side by Side</h2><p>The persuasive force of the juxtaposition, of which the introductory chapter
spoke, rests on two conditions. The first is simultaneity: the viewer should see
both answers come into being at the very same moment. The second is equality of
conditions: both answers must be given to the same question and under the same
premises, so that the visible difference is attributable solely to the attachment
of the server and not to some incidental unequal treatment in the construction.
Neither condition is self-evident; both are worked into the source. This chapter
shows how.</p><p>Equality of conditions begins with the layout of the view. For each of the two
columns there is a field of the same kind; what exists on the left exists on the
right in the same shape.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">TextArea</span><span class="w"/><span class="n">questionInput</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">TextArea</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">Checkbox</span><span class="w"/><span class="n">mcpToggle</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Checkbox</span><span class="p">(</span><span class="s">"Enable MCP"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">Button</span><span class="w"/><span class="n">sendButton</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Button</span><span class="p">(</span><span class="s">"Ask"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">PLACEHOLDER_PLAIN</span><span class="w"/><span class="o">=</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"_Generic answers will appear here..._"</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">PLACEHOLDER_MCP</span><span class="w"/><span class="o">=</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"_Answers using your pantry will appear here..._"</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">Markdown</span><span class="w"/><span class="n">leftBody</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Markdown</span><span class="p">(</span><span class="n">PLACEHOLDER_PLAIN</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">Markdown</span><span class="w"/><span class="n">rightBody</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Markdown</span><span class="p">(</span><span class="n">PLACEHOLDER_MCP</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">StringBuilder</span><span class="w"/><span class="n">leftBuffer</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">StringBuilder</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">StringBuilder</span><span class="w"/><span class="n">rightBuffer</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">StringBuilder</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">ChatProgressView</span><span class="w"/><span class="n">leftProgress</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgressView</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">ChatProgressView</span><span class="w"/><span class="n">rightProgress</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgressView</span><span class="p">();</span></span></span></code></pre></div></div><p>Each column is assigned its own<code>Markdown</code> component for the finished answer, its
own buffer for the incoming text, and its own progress view. This paired layout is
the structural basis of the symmetry; it ensures that the left and right columns
are cut from the same cloth.</p><p>The symmetry shows itself more emphatically still in the fact that both columns
issue from a single method. A boolean parameter decides whether the plain or the
MCP-enabled variant arises.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="n">Div</span><span class="w"/><span class="nf">buildAnswerColumn</span><span class="p">(</span><span class="kt">boolean</span><span class="w"/><span class="n">mcp</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">col</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">col</span><span class="p">.</span><span class="na">addClassNames</span><span class="p">(</span><span class="s">"recipe-answer"</span><span class="p">,</span><span class="w"/><span class="n">mcp</span><span class="w"/><span class="o">?</span><span class="w"/><span class="s">"recipe-answer--mcp"</span><span class="w"/><span class="p">:</span><span class="w"/><span class="s">"recipe-answer--plain"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">chip</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">chip</span><span class="p">.</span><span class="na">addClassNames</span><span class="p">(</span><span class="s">"recipe-chip"</span><span class="p">,</span><span class="w"/><span class="n">mcp</span><span class="w"/><span class="o">?</span><span class="w"/><span class="s">"recipe-chip--mcp"</span><span class="w"/><span class="p">:</span><span class="w"/><span class="s">"recipe-chip--plain"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">mcp</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">chip</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">Icon</span><span class="p">(</span><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">MAGIC</span><span class="p">),</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="s">"With MCP"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">else</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">chip</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">Icon</span><span class="p">(</span><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">COMMENT_O</span><span class="p">),</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="s">"Without MCP"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">header</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">(</span><span class="n">chip</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">header</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-answer__header"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">subtitle</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="n">mcp</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">?</span><span class="w"/><span class="s">"Has access to your pantry &amp; profile"</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">:</span><span class="w"/><span class="s">"Generic recipe knowledge only"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">subtitle</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-card__subtitle"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ChatProgressView</span><span class="w"/><span class="n">progress</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">mcp</span><span class="w"/><span class="o">?</span><span class="w"/><span class="n">rightProgress</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">leftProgress</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Markdown</span><span class="w"/><span class="n">body</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">mcp</span><span class="w"/><span class="o">?</span><span class="w"/><span class="n">rightBody</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">leftBody</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">body</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-answer__body"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">col</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">header</span><span class="p">,</span><span class="w"/><span class="n">subtitle</span><span class="p">,</span><span class="w"/><span class="n">progress</span><span class="p">,</span><span class="w"/><span class="n">body</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">col</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>The two columns are produced in one place by two calls with the values<code>false</code> and<code>true</code>; the left, accordingly, is the plain one, the right the MCP-enabled one.
Apart from the labelling, the colouring and the choice of the associated field
pair, the construction is identical. Anyone searching the code for a difference
between the columns finds it solely in that one boolean value — and this is
precisely the assurance, visible in the source, that the juxtaposition is honest.</p><p>Simultaneity, in turn, arises on triggering via the button. The method<code>handleSend</code> prepares the interface and then sets the concurrent processing in
motion.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">handleSend</span><span class="p">()</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">question</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">questionInput</span><span class="p">.</span><span class="na">getValue</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">question</span><span class="w"/><span class="o">==</span><span class="w"/><span class="kc">null</span><span class="w"/><span class="o">||</span><span class="w"/><span class="n">question</span><span class="p">.</span><span class="na">isBlank</span><span class="p">())</span><span class="w"/><span class="k">return</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">leftBuffer</span><span class="p">.</span><span class="na">setLength</span><span class="p">(</span><span class="n">0</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">rightBuffer</span><span class="p">.</span><span class="na">setLength</span><span class="p">(</span><span class="n">0</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">leftBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="s">""</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">rightBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="s">""</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">leftProgress</span><span class="p">.</span><span class="na">clear</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">clear</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">sendButton</span><span class="p">.</span><span class="na">setEnabled</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">UI</span><span class="w"/><span class="n">ui</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">UI</span><span class="p">.</span><span class="na">getCurrent</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kt">boolean</span><span class="w"/><span class="n">withMcp</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">mcpToggle</span><span class="p">.</span><span class="na">getValue</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">Boolean</span><span class="o">&gt;</span><span class="w"/><span class="n">leftDone</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;&gt;</span><span class="p">(</span><span class="n">Boolean</span><span class="p">.</span><span class="na">FALSE</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">Boolean</span><span class="o">&gt;</span><span class="w"/><span class="n">rightDone</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;&gt;</span><span class="p">(</span><span class="o">!</span><span class="n">withMcp</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Thread</span><span class="p">.</span><span class="na">startVirtualThread</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">runPlain</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span><span class="w"/><span class="n">question</span><span class="p">,</span><span class="w"/><span class="n">leftDone</span><span class="p">,</span><span class="w"/><span class="n">rightDone</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">withMcp</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Thread</span><span class="p">.</span><span class="na">startVirtualThread</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">runWithMcp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span><span class="w"/><span class="n">question</span><span class="p">,</span><span class="w"/><span class="n">leftDone</span><span class="p">,</span><span class="w"/><span class="n">rightDone</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">else</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="s">"_(MCP disabled - check the box to enable.)_"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>After resetting both panels and disabling the button, two mutually independent
virtual threads are started — one for the plain request, one for the MCP-enabled
one. Virtual threads are the fitting means here: they are so lightweight that one
may be created for each request without hesitation, and they may block while
waiting for the answer to arrive without tying up a platform resource. The plain
request is always made; the MCP-enabled one only when the toggle is active.
Otherwise the right column receives a plain note.</p><p>The division of labour behind the two threads deserves a remark of its own. The
plain request drives the method<code>runPlain</code>, the MCP-enabled one the method<code>runWithMcp</code>; the latter makes use of the same<code>WalkthroughEngine</code> that also drives
the step-by-step view of the second part — here merely as an automatic loop with
no pauses between the phases. The actual protocol logic thus resides in exactly one
place, and only the drive differs. The bodies of both methods contain the
streaming callback that conveys the incoming text fragments into the interface; the
following chapter is devoted to it, which is why it is omitted here.</p><p>There remains the joining of the two strands. Each thread notes at its end that it
has finished, and then calls a plain coordination method.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">maybeReenable</span><span class="p">(</span><span class="n">UI</span><span class="w"/><span class="n">ui</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">Boolean</span><span class="o">&gt;</span><span class="w"/><span class="n">leftDone</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">Boolean</span><span class="o">&gt;</span><span class="w"/><span class="n">rightDone</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">leftDone</span><span class="p">.</span><span class="na">get</span><span class="p">()</span><span class="w"/><span class="o">&amp;&amp;</span><span class="w"/><span class="n">rightDone</span><span class="p">.</span><span class="na">get</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">sendButton</span><span class="p">.</span><span class="na">setEnabled</span><span class="p">(</span><span class="kc">true</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>Only once both flags are set is the button re-enabled. Since the two flags are set
and read in separate threads, they are realised as<code>AtomicReference</code>, whose value
is safely visible across thread boundaries. Here the initial assignment mentioned
earlier pays off: the right-hand flag is pre-set with<code>!withMcp</code> and therefore
counts as done at once when the toggle is inactive. Thus, in plain operation, the
single thread of the left column suffices to re-enable the button, whereas in MCP
operation both threads are awaited. No separate synchronisation is needed; the
logic carries itself.</p><p>The whole sequence can be read as a fork and a join.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-diagram-02.png" alt="Fork-and-join of the two model requests" loading="lazy" decoding="async"/><p>Whereas the diagram shows the structure of the fork, the following figure makes the
temporal side visible: both strands genuinely run within the same span of time, and
the MCP-enabled lane lasts longer because it calls tools along the way.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-ch03.png" alt="Two requests on one timeline: both strands run concurrently, the MCP lane lasts longer" loading="lazy" decoding="async"/><p>With that, the two conditions on which the juxtaposition rests are met. The
symmetry of construction ensures that both answers arise under equal premises; the
concurrent conduct over two virtual threads ensures that they do so at the same
moment. What has remained open so far is the question of how the text fragments
arriving in the background threads find their way into the server-side interface at
all. It is to this very question that the following chapter turns.</p><h2 id="4-channelling-a-character-stream-into-the-component-tree">4. Channelling a Character Stream into the Component Tree</h2><p>The preceding chapter left a question open: how the text fragments arriving in the
two background threads find their way into the interface at all. Three obstacles
stand in the way. First, Vaadin keeps the component tree on the server; the model&rsquo;s
answer, however, does not arise there but arrives over the network. Second, the
component tree may not be altered from an arbitrary thread, but only within the
locked context of the associated session. Third, the browser does not poll for
updates of its own accord; without further intervention the picture would stand
still until the user reloads the page. This chapter shows how these three obstacles
are overcome together — along the path that a single fragment travels.</p><p>At the beginning of this path stands the stream itself. The<code>OllamaClient</code> requests
the model&rsquo;s answer with streaming enabled and reads it as Server-Sent Events. The
plain request of the left column runs through the method<code>chatPlain</code>, which joins
the system prompt and the question into a message list and then calls the inner
streaming method. The third parameter is noteworthy: a<code>Consumer&lt;String&gt;</code> to which
every incoming text fragment is handed.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">chatPlain</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">systemPrompt</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">userMessage</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Consumer</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="w"/><span class="n">textChunk</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Consumer</span><span class="o">&lt;</span><span class="n">ChatProgress</span><span class="o">&gt;</span><span class="w"/><span class="n">progress</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Instant</span><span class="w"/><span class="n">start</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">progress</span><span class="p">.</span><span class="na">accept</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">info</span><span class="p">(</span><span class="s">"Sending request to Ollama"</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"model: "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">model</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">List</span><span class="o">&lt;</span><span class="n">ObjectNode</span><span class="o">&gt;</span><span class="w"/><span class="n">messages</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ArrayList</span><span class="o">&lt;&gt;</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">messages</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">message</span><span class="p">(</span><span class="s">"system"</span><span class="p">,</span><span class="w"/><span class="n">systemPrompt</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">messages</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">message</span><span class="p">(</span><span class="s">"user"</span><span class="p">,</span><span class="w"/><span class="n">userMessage</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">progress</span><span class="p">.</span><span class="na">accept</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">stream</span><span class="p">(</span><span class="s">"Streaming reply..."</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">StreamResult</span><span class="w"/><span class="n">outcome</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">streamOnce</span><span class="p">(</span><span class="n">messages</span><span class="p">,</span><span class="w"/><span class="kc">null</span><span class="p">,</span><span class="w"/><span class="n">textChunk</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">progress</span><span class="p">.</span><span class="na">accept</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">success</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"Reply complete ("</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">outcome</span><span class="p">.</span><span class="na">assistantText</span><span class="p">.</span><span class="na">length</span><span class="p">()</span><span class="w"/><span class="o">+</span><span class="w"/><span class="s">" chars)"</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Duration</span><span class="p">.</span><span class="na">between</span><span class="p">(</span><span class="n">start</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">())));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">RuntimeException</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">progress</span><span class="p">.</span><span class="na">accept</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">error</span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="na">getMessage</span><span class="p">()));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">throw</span><span class="w"/><span class="n">e</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>The method is given two callbacks: one for the text fragments, the other for
structured progress events. The actual stream<code>chatPlain</code> leaves to the inner
method<code>streamOnce</code>, to which it passes the text callback unchanged; the<code>null</code>
supplied in place of the tool list marks the plain request.</p><p>The actual decomposition of the stream takes place in this inner method. It reads
the answer line by line; empty lines and lines not belonging to the stream are
skipped, and the sentinel line<code>[DONE]</code> ends the loop.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">(</span><span class="n">BufferedReader</span><span class="w"/><span class="n">reader</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">BufferedReader</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">new</span><span class="w"/><span class="n">InputStreamReader</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="na">body</span><span class="p">(),</span><span class="w"/><span class="n">StandardCharsets</span><span class="p">.</span><span class="na">UTF_8</span><span class="p">)))</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">line</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">while</span><span class="w"/><span class="p">((</span><span class="n">line</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">reader</span><span class="p">.</span><span class="na">readLine</span><span class="p">())</span><span class="w"/><span class="o">!=</span><span class="w"/><span class="kc">null</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">line</span><span class="p">.</span><span class="na">isEmpty</span><span class="p">())</span><span class="w"/><span class="k">continue</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="o">!</span><span class="n">line</span><span class="p">.</span><span class="na">startsWith</span><span class="p">(</span><span class="s">"data:"</span><span class="p">))</span><span class="w"/><span class="k">continue</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">data</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">line</span><span class="p">.</span><span class="na">substring</span><span class="p">(</span><span class="n">5</span><span class="p">).</span><span class="na">trim</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="s">"[DONE]"</span><span class="p">.</span><span class="na">equals</span><span class="p">(</span><span class="n">data</span><span class="p">))</span><span class="w"/><span class="k">break</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">JsonNode</span><span class="w"/><span class="n">event</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">event</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">MAPPER</span><span class="p">.</span><span class="na">readTree</span><span class="p">(</span><span class="n">data</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">Exception</span><span class="w"/><span class="n">parseError</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">logger</span><span class="p">().</span><span class="na">warn</span><span class="p">(</span><span class="s">"Skipping malformed SSE chunk: {}"</span><span class="p">,</span><span class="w"/><span class="n">data</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">continue</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">JsonNode</span><span class="w"/><span class="n">choice</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">event</span><span class="p">.</span><span class="na">path</span><span class="p">(</span><span class="s">"choices"</span><span class="p">).</span><span class="na">path</span><span class="p">(</span><span class="n">0</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">JsonNode</span><span class="w"/><span class="n">delta</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">choice</span><span class="p">.</span><span class="na">path</span><span class="p">(</span><span class="s">"delta"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">delta</span><span class="p">.</span><span class="na">hasNonNull</span><span class="p">(</span><span class="s">"content"</span><span class="p">))</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">piece</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">delta</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="s">"content"</span><span class="p">).</span><span class="na">asText</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="o">!</span><span class="n">piece</span><span class="p">.</span><span class="na">isEmpty</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">assistantText</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">piece</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">textChunk</span><span class="w"/><span class="o">!=</span><span class="w"/><span class="kc">null</span><span class="p">)</span><span class="w"/><span class="n">textChunk</span><span class="p">.</span><span class="na">accept</span><span class="p">(</span><span class="n">piece</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">delta</span><span class="p">.</span><span class="na">hasNonNull</span><span class="p">(</span><span class="s">"tool_calls"</span><span class="p">))</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">JsonNode</span><span class="w"/><span class="n">tc</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">delta</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="s">"tool_calls"</span><span class="p">))</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kt">int</span><span class="w"/><span class="n">idx</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">tc</span><span class="p">.</span><span class="na">path</span><span class="p">(</span><span class="s">"index"</span><span class="p">).</span><span class="na">asInt</span><span class="p">(</span><span class="n">0</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ToolCallAccumulator</span><span class="w"/><span class="n">acc</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">calls</span><span class="p">.</span><span class="na">computeIfAbsent</span><span class="p">(</span><span class="n">idx</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">k</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ToolCallAccumulator</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">tc</span><span class="p">.</span><span class="na">hasNonNull</span><span class="p">(</span><span class="s">"id"</span><span class="p">))</span><span class="w"/><span class="n">acc</span><span class="p">.</span><span class="na">id</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">tc</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="s">"id"</span><span class="p">).</span><span class="na">asText</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">JsonNode</span><span class="w"/><span class="n">fn</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">tc</span><span class="p">.</span><span class="na">path</span><span class="p">(</span><span class="s">"function"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">fn</span><span class="p">.</span><span class="na">hasNonNull</span><span class="p">(</span><span class="s">"name"</span><span class="p">))</span><span class="w"/><span class="n">acc</span><span class="p">.</span><span class="na">name</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">fn</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="s">"name"</span><span class="p">).</span><span class="na">asText</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">fn</span><span class="p">.</span><span class="na">hasNonNull</span><span class="p">(</span><span class="s">"arguments"</span><span class="p">))</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">acc</span><span class="p">.</span><span class="na">argumentsBuffer</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">fn</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="s">"arguments"</span><span class="p">).</span><span class="na">asText</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">finish</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">choice</span><span class="p">.</span><span class="na">path</span><span class="p">(</span><span class="s">"finish_reason"</span><span class="p">).</span><span class="na">asText</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">finish</span><span class="w"/><span class="o">!=</span><span class="w"/><span class="kc">null</span><span class="w"/><span class="o">&amp;&amp;</span><span class="w"/><span class="o">!</span><span class="n">finish</span><span class="p">.</span><span class="na">isBlank</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">break</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>For the text path, only the middle section matters: if the delta carries content,
that portion is appended to the gathered answer text and at the same time handed to
the callback. In this way every fragment leaves the method at the very moment it
arrives. The section that follows gathers the components of any tool calls; it is
left undiscussed here and is the subject of the sixth chapter. The appearance of a
finish reason ends the loop.</p><p>With that, the source is clarified: the<code>Consumer&lt;String&gt;</code> receives the fragment.
Before it can appear in the browser, however, the first of the named obstacles must
be taken — the browser must be allowed to be supplied unsolicited. This is what the
annotation<code>@Push</code> does, which hangs not on an individual view but on the central<code>AppShell</code> and therefore applies to the whole application.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kn">package</span><span class="w"/><span class="nn">com.svenruppert.mcp.ui</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"/><span class="nn">com.vaadin.flow.component.page.AppShellConfigurator</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"/><span class="nn">com.vaadin.flow.component.page.Push</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"/><span class="nn">com.vaadin.flow.theme.Theme</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"/><span class="nn">com.vaadin.flow.theme.lumo.Lumo</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="nd">@Push</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="nd">@Theme</span><span class="p">(</span><span class="n">value</span><span class="w"/><span class="o">=</span><span class="w"/><span class="s">"recipe"</span><span class="p">,</span><span class="w"/><span class="n">variant</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">Lumo</span><span class="p">.</span><span class="na">LIGHT</span><span class="p">)</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="kd">public</span><span class="w"/><span class="kd">class</span><span class="nc">AppShell</span><span class="w"/><span class="kd">implements</span><span class="w"/><span class="n">AppShellConfigurator</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>The annotation permits the server to send changes to the browser of its own
accord — over the WebSocket channel that was configured expressly in the second
chapter. Because it sits on the<code>AppShell</code>, it applies to both views alike; the
second annotation merely determines the appearance.</p><p>There remains the second obstacle: the change in the correct context. The receiving
end in the main view is the method<code>runPlain</code>. It passes<code>chatPlain</code> a callback that
processes every fragment by way of<code>UI.access</code>.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">runPlain</span><span class="p">(</span><span class="n">UI</span><span class="w"/><span class="n">ui</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">question</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">Boolean</span><span class="o">&gt;</span><span class="w"/><span class="n">leftDone</span><span class="p">,</span><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">Boolean</span><span class="o">&gt;</span><span class="w"/><span class="n">rightDone</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">services</span><span class="p">.</span><span class="na">ollama</span><span class="p">().</span><span class="na">chatPlain</span><span class="p">(</span><span class="n">Prompts</span><span class="p">.</span><span class="na">SYSTEM_PLAIN</span><span class="p">,</span><span class="w"/><span class="n">question</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">chunk</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">leftBuffer</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">chunk</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">leftBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="n">leftBuffer</span><span class="p">.</span><span class="na">toString</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}),</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ev</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">leftProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">ev</span><span class="p">)));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">Exception</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">logger</span><span class="p">().</span><span class="na">warn</span><span class="p">(</span><span class="s">"Plain chat failed"</span><span class="p">,</span><span class="w"/><span class="n">e</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">leftBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="s">"**Error:** "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">e</span><span class="p">.</span><span class="na">getMessage</span><span class="p">()));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">finally</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">leftDone</span><span class="p">.</span><span class="na">set</span><span class="p">(</span><span class="n">Boolean</span><span class="p">.</span><span class="na">TRUE</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">maybeReenable</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span><span class="w"/><span class="n">leftDone</span><span class="p">,</span><span class="w"/><span class="n">rightDone</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>Here the threads come together. The reference to the<code>UI</code> was captured earlier in
the triggering thread and handed along into the background thread. Every incoming
fragment is not processed directly but wrapped in a<code>UI.access</code> call. This is
precisely the key: altering the component tree from a foreign thread would be
impermissible, since it would bypass the session&rsquo;s lock and lead to race
conditions.<code>UI.access</code> instead enqueues the change into the locked context of the
session and, since<code>@Push</code> is active, at the same time sets its transmission to the
browser in motion. Thus a single call overcomes the second and the third obstacle
in one.</p><p>Within the callback, finally, the buffer pattern shows itself. Every fragment is
appended to a<code>StringBuilder</code>, and the entire accumulated content is set as the new
content of the<code>Markdown</code> component. That the whole text is set anew on every
fragment rather than appended to the existing content is deliberate: the approach
is plain, self-contained, and matches the interface of the component, which takes a
complete text. The progress callback proceeds in the same way. At the end the
method notes in the<code>finally</code> block that the left column is finished and calls the
coordination method considered in the previous chapter; the error path sets an
error message, likewise by way of<code>UI.access</code>.</p><p>The whole path of a fragment can be read as a sequence of a few steps.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-diagram-03.png" alt="The path of a streamed text fragment into the component tree" loading="lazy" decoding="async"/><p>With that, the three obstacles named at the outset are taken together: the stream
is decomposed in the client,<code>@Push</code> permits the unsolicited supply of the browser,
and<code>UI.access</code> runs every fragment in the correct context and brings about its
transmission. One detail remains conspicuous: on every fragment the entire
accumulated text is set anew on the<code>Markdown</code> component, so that the component
displays an ever-growing, still-incomplete document. What the viewer perceives while
the document is incomplete — and whether this is the right thing — is the question
to which the following chapter turns.</p><h2 id="5-rendering-the-stream-as-markdown">5. Rendering the Stream as Markdown</h2><p>The fourth chapter traced the path of a fragment into the component tree and, at
the end, singled out one detail: on every fragment the entire accumulated text is
set on the<code>Markdown</code> component. What stood there as an incidental observation is
the actual subject of this chapter. Where the previous chapter asked how a fragment
reaches the interface, this one asks what the interface visibly does with it. The
heart of the callback reads:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="n">chunk</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">leftBuffer</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">chunk</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">leftBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="n">leftBuffer</span><span class="p">.</span><span class="na">toString</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}),</span></span></span></code></pre></div></div><p>The field<code>leftBody</code> is a<code>com.vaadin.flow.component.markdown.Markdown</code>, not a plain
text component. Its<code>setContent</code> call replaces the previous content entirely; the
component discards its existing rendering and re-renders the whole accumulated text.
Since this happens on every fragment, the answer appears to the viewer not as
growing raw text but as a continuously rendered Markdown document.</p><p>For the viewer, this means that the answer builds itself up as a structured
document. A heading appears as a heading as soon as its line is complete; a list
appears as a list as soon as its entries arrive; an emphasis takes effect as soon
as it is closed. The document thus comes into being not as a draft to be put into
shape at the end, but already in its final form, step by step.</p><p>This approach has a price. As long as a piece of markup is still incomplete — an
emphasis opened but not yet closed, a list begun but not yet finished, a code block
that has arrived only halfway — the component renders that intermediate state just
as it currently stands. If the missing part arrives shortly afterwards, the
affected section rearranges itself. The viewer perceives this as a brief
reordering that resolves itself as the remainder arrives.</p><p>The main view accepts this occasional reordering deliberately, and the reason lies
in its purpose. It is the comparison view, and its entire persuasive force rests,
as the introductory chapter set out, on the immediacy of the juxtaposition. The
viewer should see both answers come into being vividly and at the very same moment;
the progressive rendering serves precisely this vividness. The answers are,
moreover, manageable recipe texts in which the reordering is rare and slight. The
small price thus stands in a reasonable proportion to the impression of simultaneity
that is gained.</p><p>It should not be passed over that the same matter can be solved differently — and is
solved differently in the same project. The step-by-step view takes the opposite
path: it shows the stream first as raw text and renders it as Markdown only after it
has completed.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">query</span><span class="p">.</span><span class="na">toolCalls</span><span class="p">().</span><span class="na">isEmpty</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">result</span><span class="p">.</span><span class="na">remove</span><span class="p">(</span><span class="n">streamDiv</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Markdown</span><span class="w"/><span class="n">rendered</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Markdown</span><span class="p">(</span><span class="n">streamedText</span><span class="p">.</span><span class="na">isBlank</span><span class="p">()</span><span class="w"/><span class="o">?</span><span class="w"/><span class="s">"_(no text)_"</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">streamedText</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">rendered</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__rendered"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">result</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">rendered</span><span class="p">);</span></span></span></code></pre></div></div><p>During the stream the text grows there in a plain area; only after completion is
that area removed and replaced by a freshly created<code>Markdown</code> component. The
corresponding phase description states it expressly: &ldquo;Streaming is complete and the
text is rendered as Markdown.&rdquo; The reason for this different choice lies in the
different purpose: a teaching view prefers a calm, stable picture of each phase that
the viewer can study at his own pace, without the rendered output rearranging itself
beneath his gaze. The closer examination of this counter-design belongs in the
second part; here the note suffices that these are two well-founded answers to the
same question.</p><p>The two strategies can be set side by side in brief.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-diagram-04.png" alt="Two rendering strategies — progressive vs deferred Markdown" loading="lazy" decoding="async"/><p>Both paths follow the same principle, that an interface should honestly reflect the
state of its content — they merely draw different conclusions from it. The
comparison view shows the document as it forms, because it cares about the
simultaneity of its emergence. The teaching view shows each phase as it stands once
complete, because it cares about the calm of study. What neither view has so far
disclosed is the traffic that makes the right-hand answer possible in the first
place — the calls to the tools and their results. It is to this very traffic, and to
making it visible, that the following chapter turns.</p><h2 id="6-making-the-protocol-visible">6. Making the Protocol Visible</h2><p>The previous chapter closed with an observation: what neither view had so far
disclosed is the traffic that makes the right-hand answer possible in the first
place. This traffic is precisely what MCP achieves — and at the same time what
remains hidden in normal operation. The fetching of the tool list, the individual
tool calls, their results and the repeated model runs all happen behind the scenes.
Without making them visible, the right column would be a mere black box that
delivers a better answer but does not show why. A comparison view meant to instruct
cannot leave it at that. The main view therefore makes the traffic legible — by way
of a vertical timeline that runs alongside each answer.</p><p>At the beginning stands a plain event model. Every step of the processing is
recorded as a<code>ChatProgress</code>, a<code>record</code> with a small, closed enumeration of event
kinds and a factory method for each.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"/><span class="kd">record</span><span class="nc">ChatProgress</span><span class="p">(</span><span class="n">Kind</span><span class="w"/><span class="n">kind</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">detail</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="w"/><span class="n">at</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">enum</span><span class="w"/><span class="n">Kind</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">INFO</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">STREAM</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">TOOL_CALL</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">TOOL_RESULT</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">SUCCESS</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ERROR</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">ChatProgress</span><span class="w"/><span class="nf">info</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">label</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgress</span><span class="p">(</span><span class="n">Kind</span><span class="p">.</span><span class="na">INFO</span><span class="p">,</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="kc">null</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">ChatProgress</span><span class="w"/><span class="nf">info</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">detail</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgress</span><span class="p">(</span><span class="n">Kind</span><span class="p">.</span><span class="na">INFO</span><span class="p">,</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">detail</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">ChatProgress</span><span class="w"/><span class="nf">stream</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">label</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgress</span><span class="p">(</span><span class="n">Kind</span><span class="p">.</span><span class="na">STREAM</span><span class="p">,</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="kc">null</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">ChatProgress</span><span class="w"/><span class="nf">toolCall</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">detail</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgress</span><span class="p">(</span><span class="n">Kind</span><span class="p">.</span><span class="na">TOOL_CALL</span><span class="p">,</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">detail</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">ChatProgress</span><span class="w"/><span class="nf">toolResult</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">detail</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgress</span><span class="p">(</span><span class="n">Kind</span><span class="p">.</span><span class="na">TOOL_RESULT</span><span class="p">,</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">detail</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">ChatProgress</span><span class="w"/><span class="nf">success</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">Duration</span><span class="w"/><span class="n">elapsed</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">d</span><span class="w"/><span class="o">=</span><span class="w"/><span class="s">"%.1fs"</span><span class="p">.</span><span class="na">formatted</span><span class="p">(</span><span class="n">elapsed</span><span class="p">.</span><span class="na">toMillis</span><span class="p">()</span><span class="w"/><span class="o">/</span><span class="w"/><span class="n">1000</span><span class="p">.</span><span class="na">0</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgress</span><span class="p">(</span><span class="n">Kind</span><span class="p">.</span><span class="na">SUCCESS</span><span class="p">,</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">d</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">ChatProgress</span><span class="w"/><span class="nf">error</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">message</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgress</span><span class="p">(</span><span class="n">Kind</span><span class="p">.</span><span class="na">ERROR</span><span class="p">,</span><span class="w"/><span class="s">"Error"</span><span class="p">,</span><span class="w"/><span class="n">message</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>Six event kinds suffice to describe the whole course of an MCP run: a general
notice, the beginning of a stream, a tool call, a tool result, the success and the
error. Every event carries a concise label, an optional detail text and the moment
of its creation. The detail text is the part decisive for the protocol, for it
holds the arguments of a call and the returned result.</p><p>The display of these events is taken on by the<code>ChatProgressView</code>, a vertical
timeline. Its central method appends a row for each event.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">append</span><span class="p">(</span><span class="n">ChatProgress</span><span class="w"/><span class="n">event</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">row</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">row</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-progress__row"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">row</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-progress__row--"</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">event</span><span class="p">.</span><span class="na">kind</span><span class="p">().</span><span class="na">name</span><span class="p">().</span><span class="na">toLowerCase</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Icon</span><span class="w"/><span class="n">icon</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Icon</span><span class="p">(</span><span class="n">iconFor</span><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="na">kind</span><span class="p">()));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">icon</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-progress__icon"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">time</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="n">TIME_FMT</span><span class="p">.</span><span class="na">format</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">LocalTime</span><span class="p">.</span><span class="na">ofInstant</span><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="na">at</span><span class="p">(),</span><span class="w"/><span class="n">ZoneId</span><span class="p">.</span><span class="na">systemDefault</span><span class="p">())));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">time</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-progress__time"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">label</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="na">label</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">label</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-progress__label"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">head</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">(</span><span class="n">icon</span><span class="p">,</span><span class="w"/><span class="n">time</span><span class="p">,</span><span class="w"/><span class="n">label</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">head</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-progress__head"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">row</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">head</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="na">detail</span><span class="p">()</span><span class="w"/><span class="o">!=</span><span class="w"/><span class="kc">null</span><span class="w"/><span class="o">&amp;&amp;</span><span class="w"/><span class="o">!</span><span class="n">event</span><span class="p">.</span><span class="na">detail</span><span class="p">().</span><span class="na">isBlank</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Pre</span><span class="w"/><span class="n">detail</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Pre</span><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="na">detail</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">detail</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-progress__detail"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">row</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">detail</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">add</span><span class="p">(</span><span class="n">row</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">getElement</span><span class="p">().</span><span class="na">executeJs</span><span class="p">(</span><span class="s">"this.scrollTop = this.scrollHeight"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">VaadinIcon</span><span class="w"/><span class="nf">iconFor</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">Kind</span><span class="w"/><span class="n">k</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">switch</span><span class="w"/><span class="p">(</span><span class="n">k</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">case</span><span class="w"/><span class="n">INFO</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">INFO_CIRCLE_O</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">case</span><span class="w"/><span class="n">STREAM</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">PAPERPLANE</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">case</span><span class="w"/><span class="n">TOOL_CALL</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">COG</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">case</span><span class="w"/><span class="n">TOOL_RESULT</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">CHECK_CIRCLE_O</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">case</span><span class="w"/><span class="n">SUCCESS</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">CHECK_CIRCLE</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">case</span><span class="w"/><span class="n">ERROR</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">WARNING</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">};</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>Each row consists of a head — an icon suited to the event kind, the time and the
label — and, if a detail text is present, a<code>Pre</code> block set beneath it. This very<code>Pre</code> block carries the content in a monospaced and thus legibly arranged form:
here the arguments of a call and the result JSON appear in a shape that the viewer
can follow character by character. The event kind also determines a style class of
its own, so that calls, results and errors are distinguished by colour as well.
Finally, the method scrolls to the most recent entry by way of a small JavaScript
call, so that the bar always shows the current happening.</p><p>The events are created where the traffic actually takes place — in the method<code>runWithMcp</code>, which drives the MCP-enabled request.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">runWithMcp</span><span class="p">(</span><span class="n">UI</span><span class="w"/><span class="n">ui</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">question</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">Boolean</span><span class="o">&gt;</span><span class="w"/><span class="n">leftDone</span><span class="p">,</span><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">Boolean</span><span class="o">&gt;</span><span class="w"/><span class="n">rightDone</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughEngine</span><span class="w"/><span class="n">engine</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">services</span><span class="p">.</span><span class="na">engine</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Instant</span><span class="w"/><span class="n">start</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughState</span><span class="w"/><span class="n">state</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">engine</span><span class="p">.</span><span class="na">startRun</span><span class="p">(</span><span class="n">Prompts</span><span class="p">.</span><span class="na">SYSTEM_MCP</span><span class="p">,</span><span class="w"/><span class="n">question</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ChatProgress</span><span class="p">.</span><span class="na">info</span><span class="p">(</span><span class="s">"Fetching tool list from MCP server"</span><span class="p">)));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ToolCatalog</span><span class="w"/><span class="n">catalog</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">engine</span><span class="p">.</span><span class="na">fetchToolCatalogue</span><span class="p">(</span><span class="n">state</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ChatProgress</span><span class="p">.</span><span class="na">info</span><span class="p">(</span><span class="s">"Loaded "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">catalog</span><span class="p">.</span><span class="na">toolNames</span><span class="p">().</span><span class="na">size</span><span class="p">()</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">+</span><span class="w"/><span class="s">" tools from MCP server"</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="p">.</span><span class="na">join</span><span class="p">(</span><span class="s">", "</span><span class="p">,</span><span class="w"/><span class="n">catalog</span><span class="p">.</span><span class="na">toolNames</span><span class="p">()))));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="kt">int</span><span class="w"/><span class="n">safety</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">0</span><span class="p">;</span><span class="w"/><span class="n">safety</span><span class="w"/><span class="o">&lt;</span><span class="w"/><span class="n">WalkthroughEngine</span><span class="p">.</span><span class="na">MAX_ITERATIONS</span><span class="p">;</span><span class="w"/><span class="n">safety</span><span class="o">++</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">final</span><span class="w"/><span class="kt">int</span><span class="w"/><span class="n">iterNumber</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">state</span><span class="p">.</span><span class="na">iteration</span><span class="p">()</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">1</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">stream</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"Iteration "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">iterNumber</span><span class="w"/><span class="o">+</span><span class="w"/><span class="s">": streaming reply..."</span><span class="p">)));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ModelQuery</span><span class="w"/><span class="n">query</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">engine</span><span class="p">.</span><span class="na">runModelQuery</span><span class="p">(</span><span class="n">state</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">chunk</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">rightBuffer</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">chunk</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">rightBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="n">rightBuffer</span><span class="p">.</span><span class="na">toString</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">query</span><span class="p">.</span><span class="na">toolCalls</span><span class="p">().</span><span class="na">isEmpty</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Duration</span><span class="w"/><span class="n">elapsed</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">Duration</span><span class="p">.</span><span class="na">between</span><span class="p">(</span><span class="n">start</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kt">int</span><span class="w"/><span class="n">it</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">state</span><span class="p">.</span><span class="na">iteration</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">success</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"Reply complete after "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">it</span><span class="w"/><span class="o">+</span><span class="w"/><span class="s">" iteration"</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">+</span><span class="w"/><span class="p">(</span><span class="n">it</span><span class="w"/><span class="o">==</span><span class="w"/><span class="n">1</span><span class="w"/><span class="o">?</span><span class="w"/><span class="s">""</span><span class="w"/><span class="p">:</span><span class="w"/><span class="s">"s"</span><span class="p">),</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">elapsed</span><span class="p">)));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">PendingToolCall</span><span class="w"/><span class="n">pc</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">query</span><span class="p">.</span><span class="na">toolCalls</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">toolCall</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"Call "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">pc</span><span class="p">.</span><span class="na">name</span><span class="p">()</span><span class="w"/><span class="o">+</span><span class="w"/><span class="s">"(...)"</span><span class="p">,</span><span class="w"/><span class="n">pc</span><span class="p">.</span><span class="na">argumentsJson</span><span class="p">())));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ToolExecution</span><span class="w"/><span class="n">exec</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">engine</span><span class="p">.</span><span class="na">executeTools</span><span class="p">(</span><span class="n">state</span><span class="p">,</span><span class="w"/><span class="n">query</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ToolOutcome</span><span class="w"/><span class="n">oc</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">exec</span><span class="p">.</span><span class="na">outcomes</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ChatProgress</span><span class="w"/><span class="n">event</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">oc</span><span class="p">.</span><span class="na">error</span><span class="p">()</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">?</span><span class="w"/><span class="n">ChatProgress</span><span class="p">.</span><span class="na">error</span><span class="p">(</span><span class="n">oc</span><span class="p">.</span><span class="na">name</span><span class="p">()</span><span class="w"/><span class="o">+</span><span class="w"/><span class="s">": "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">oc</span><span class="p">.</span><span class="na">resultText</span><span class="p">())</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">:</span><span class="w"/><span class="n">ChatProgress</span><span class="p">.</span><span class="na">toolResult</span><span class="p">(</span><span class="s">"Result from "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">oc</span><span class="p">.</span><span class="na">name</span><span class="p">(),</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">oc</span><span class="p">.</span><span class="na">resultText</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">event</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">info</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"Reached max tool-call iteration limit. Stopping."</span><span class="p">)));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightBuffer</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"\n\n[Reached the maximum tool-call iteration limit. Stopping here.]"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="n">rightBuffer</span><span class="p">.</span><span class="na">toString</span><span class="p">()));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">Exception</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">logger</span><span class="p">().</span><span class="na">warn</span><span class="p">(</span><span class="s">"MCP chat failed"</span><span class="p">,</span><span class="w"/><span class="n">e</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">error</span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="na">getMessage</span><span class="p">())));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="s">"**Error:** "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">e</span><span class="p">.</span><span class="na">getMessage</span><span class="p">()));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">finally</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">rightDone</span><span class="p">.</span><span class="na">set</span><span class="p">(</span><span class="n">Boolean</span><span class="p">.</span><span class="na">TRUE</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">maybeReenable</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span><span class="w"/><span class="n">leftDone</span><span class="p">,</span><span class="w"/><span class="n">rightDone</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(</span><span class="n">sidebar</span><span class="p">::</span><span class="n">refreshGrid</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(</span><span class="n">sidebar</span><span class="p">::</span><span class="n">refreshProfileForm</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>The method drives a loop whose inner steps — the fetching of the catalogue, the
model run, the tool execution — are performed by the<code>WalkthroughEngine</code> and are
examined more closely only in the second part. For this chapter, all that matters is
what happens between those steps: at every significant point a<code>ChatProgress</code> event
is created and appended to the timeline by way of<code>UI.access</code>. First an event
reports the fetching of the tool list, another the number and the names of the
loaded tools. In each run an event announces the beginning of the stream. If the
model requests no further tools, a success event reports the completion together
with the elapsed time. Otherwise the method creates, for each requested call, a call
event whose detail text carries the arguments, and after the execution, for each
result, a result event with the returned text — or, in the error case, an error
event. Thus, alongside the answer, a running, timestamped record of the whole
traffic comes into being.</p><p>The sequence of these events can be read as the timeline of a run.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-diagram-05.png" alt="Timeline of an MCP-enabled run (info / stream / tool calls / results / success)" loading="lazy" decoding="async"/><p>This is how the timeline looks during a run:</p><figure><img src="/images/2026/06/recipe-assistant-part-1-ch06.png" alt="A reproduction of the ChatProgressView timeline with an icon, time, label and monospaced detail block for each event" loading="lazy" decoding="async"/><p>With that, the traffic is no longer hidden. Alongside the right-hand answer the
viewer sees which tools the model requests, with which arguments it calls them and
which results flow back — and he sees it at the very moment it happens. It should be
noted that the step-by-step view presents the same traffic more elaborately still:
with one card per phase and with concurrently executed calls as side-by-side lanes
under a parallel marker. This richer, paused presentation is the subject of the
second part; the main view contents itself with the compact, running timeline.</p><p>One detail of the method already points ahead: in the<code>finally</code> block the sidebar is
refreshed after every run. Behind this lies the question of how the display of the
pantry and the profile agrees with the actual state that the tools may have changed.
It is to this very question of state consistency that the following chapter turns.</p><h2 id="7-keeping-the-state-consistent">7. Keeping the State Consistent</h2><p>The previous chapter ended with a detail that pointed ahead: in the<code>finally</code> block
of<code>runWithMcp</code> the sidebar is refreshed after every run. Behind this lies a
consistency problem to which this chapter is devoted. For two writers act upon the
same business state — the pantry and the dietary profile. The operator changes it by
hand through the maintenance sidebar; the model changes it indirectly through the
MCP tools, for instance by striking out a consumed ingredient or adjusting the
profile. A display that is to do justice to both writers must therefore be conducted
with care.</p><p>The first decision concerns the path the sidebar takes when writing. It writes
directly against the REST server and deliberately bypasses the MCP server. The
reason lies in its role: the sidebar is the operator&rsquo;s instrument, not part of the
model dialogue. The REST server holds the authoritative state; the MCP server
accesses this very state only by reading and writing through its tools. It would be
mistaken to route an operator&rsquo;s action through the model&rsquo;s tool channel. The pantry
write shows this.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">saveItem</span><span class="p">()</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">name</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">nameField</span><span class="p">.</span><span class="na">getValue</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">name</span><span class="w"/><span class="o">==</span><span class="w"/><span class="kc">null</span><span class="w"/><span class="o">||</span><span class="w"/><span class="n">name</span><span class="p">.</span><span class="na">isBlank</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">show</span><span class="p">(</span><span class="s">"Name is required"</span><span class="p">,</span><span class="w"/><span class="n">2500</span><span class="p">,</span><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">Position</span><span class="p">.</span><span class="na">MIDDLE</span><span class="p">)</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">addThemeVariants</span><span class="p">(</span><span class="n">NotificationVariant</span><span class="p">.</span><span class="na">LUMO_ERROR</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Double</span><span class="w"/><span class="n">qty</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">quantityField</span><span class="p">.</span><span class="na">getValue</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">qty</span><span class="w"/><span class="o">==</span><span class="w"/><span class="kc">null</span><span class="p">)</span><span class="w"/><span class="n">qty</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">0</span><span class="p">.</span><span class="na">0</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">unit</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">blankToDefault</span><span class="p">(</span><span class="n">unitField</span><span class="p">.</span><span class="na">getValue</span><span class="p">(),</span><span class="w"/><span class="s">"piece"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">category</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">blankToDefault</span><span class="p">(</span><span class="n">categoryField</span><span class="p">.</span><span class="na">getValue</span><span class="p">(),</span><span class="w"/><span class="s">"Pantry"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Optional</span><span class="o">&lt;</span><span class="n">LocalDate</span><span class="o">&gt;</span><span class="w"/><span class="n">bestBefore</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">Optional</span><span class="p">.</span><span class="na">ofNullable</span><span class="p">(</span><span class="n">bestBeforeField</span><span class="p">.</span><span class="na">getValue</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">PantryItem</span><span class="w"/><span class="n">item</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">PantryItem</span><span class="p">(</span><span class="n">name</span><span class="p">,</span><span class="w"/><span class="n">qty</span><span class="p">,</span><span class="w"/><span class="n">unit</span><span class="p">,</span><span class="w"/><span class="n">bestBefore</span><span class="p">,</span><span class="w"/><span class="n">category</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">nameField</span><span class="p">.</span><span class="na">isReadOnly</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">pantryClient</span><span class="p">.</span><span class="na">replace</span><span class="p">(</span><span class="n">item</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">show</span><span class="p">(</span><span class="s">"Updated "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">name</span><span class="p">,</span><span class="w"/><span class="n">1500</span><span class="p">,</span><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">Position</span><span class="p">.</span><span class="na">BOTTOM_START</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">else</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">pantryClient</span><span class="p">.</span><span class="na">create</span><span class="p">(</span><span class="n">item</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">show</span><span class="p">(</span><span class="s">"Added "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">name</span><span class="p">,</span><span class="w"/><span class="n">1500</span><span class="p">,</span><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">Position</span><span class="p">.</span><span class="na">BOTTOM_START</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">clearForm</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">refreshGrid</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">Exception</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">show</span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="na">getMessage</span><span class="p">(),</span><span class="w"/><span class="n">3000</span><span class="p">,</span><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">Position</span><span class="p">.</span><span class="na">MIDDLE</span><span class="p">)</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">addThemeVariants</span><span class="p">(</span><span class="n">NotificationVariant</span><span class="p">.</span><span class="na">LUMO_ERROR</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>After a brief check of the name, a<code>PantryItem</code> is assembled from the fields.
Whether it is created or replaced is decided by the state of the name field: if it
is read-only, an existing entry has been loaded for editing, and the call<code>replace</code>
replaces it; otherwise<code>create</code> adds a new one. What is decisive is the turn at the
end of the successful branch: the form is cleared and the grid is reloaded. This
very turn recurs on deletion.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">deleteSelected</span><span class="p">()</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Optional</span><span class="o">&lt;</span><span class="n">PantryItem</span><span class="o">&gt;</span><span class="w"/><span class="n">sel</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">grid</span><span class="p">.</span><span class="na">getSelectionModel</span><span class="p">().</span><span class="na">getFirstSelectedItem</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">sel</span><span class="p">.</span><span class="na">isEmpty</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">show</span><span class="p">(</span><span class="s">"Select an item first"</span><span class="p">,</span><span class="w"/><span class="n">1500</span><span class="p">,</span><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">Position</span><span class="p">.</span><span class="na">BOTTOM_START</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">pantryClient</span><span class="p">.</span><span class="na">delete</span><span class="p">(</span><span class="n">sel</span><span class="p">.</span><span class="na">get</span><span class="p">().</span><span class="na">name</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">show</span><span class="p">(</span><span class="s">"Removed "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">sel</span><span class="p">.</span><span class="na">get</span><span class="p">().</span><span class="na">name</span><span class="p">(),</span><span class="w"/><span class="n">1500</span><span class="p">,</span><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">Position</span><span class="p">.</span><span class="na">BOTTOM_START</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">clearForm</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">refreshGrid</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">Exception</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">show</span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="na">getMessage</span><span class="p">(),</span><span class="w"/><span class="n">3000</span><span class="p">,</span><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">Position</span><span class="p">.</span><span class="na">MIDDLE</span><span class="p">)</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">addThemeVariants</span><span class="p">(</span><span class="n">NotificationVariant</span><span class="p">.</span><span class="na">LUMO_ERROR</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>Here too the grid is reloaded after the write. With that, the actual consistency
principle shows itself, which resides in the method<code>refreshGrid</code>.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">refreshGrid</span><span class="p">()</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">grid</span><span class="p">.</span><span class="na">setItems</span><span class="p">(</span><span class="n">pantryClient</span><span class="p">.</span><span class="na">list</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">Exception</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">logger</span><span class="p">().</span><span class="na">warn</span><span class="p">(</span><span class="s">"Could not refresh pantry grid: {}"</span><span class="p">,</span><span class="w"/><span class="n">e</span><span class="p">.</span><span class="na">getMessage</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>The grid is not updated locally — the entry just created is not, say, added to the
existing display — but refilled entirely from the server. After every change the
display thus reflects the authoritative state and never a locally maintained,
possibly diverging view. This principle is plain but consequential: as long as every
change closes with a reload, the display cannot drift apart.</p><p>The writes themselves are carried out by a thin REST client using the platform&rsquo;s own
means. The replacing method may stand for them.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="n">PantryItem</span><span class="w"/><span class="nf">replace</span><span class="p">(</span><span class="n">PantryItem</span><span class="w"/><span class="n">item</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">body</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">MAPPER</span><span class="p">.</span><span class="na">writeValueAsString</span><span class="p">(</span><span class="n">item</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">HttpResponse</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="w"/><span class="n">resp</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">http</span><span class="p">.</span><span class="na">send</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">HttpRequest</span><span class="p">.</span><span class="na">newBuilder</span><span class="p">(</span><span class="n">URI</span><span class="p">.</span><span class="na">create</span><span class="p">(</span><span class="n">baseUrl</span><span class="w"/><span class="o">+</span><span class="w"/><span class="s">"/pantry/"</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">encode</span><span class="p">(</span><span class="n">item</span><span class="p">.</span><span class="na">name</span><span class="p">())))</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">timeout</span><span class="p">(</span><span class="n">Duration</span><span class="p">.</span><span class="na">ofSeconds</span><span class="p">(</span><span class="n">5</span><span class="p">))</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">header</span><span class="p">(</span><span class="s">"Content-Type"</span><span class="p">,</span><span class="w"/><span class="s">"application/json"</span><span class="p">)</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">PUT</span><span class="p">(</span><span class="n">HttpRequest</span><span class="p">.</span><span class="na">BodyPublishers</span><span class="p">.</span><span class="na">ofString</span><span class="p">(</span><span class="n">body</span><span class="p">,</span><span class="w"/><span class="n">StandardCharsets</span><span class="p">.</span><span class="na">UTF_8</span><span class="p">))</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">build</span><span class="p">(),</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">HttpResponse</span><span class="p">.</span><span class="na">BodyHandlers</span><span class="p">.</span><span class="na">ofString</span><span class="p">(</span><span class="n">StandardCharsets</span><span class="p">.</span><span class="na">UTF_8</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ensureSuccess</span><span class="p">(</span><span class="n">resp</span><span class="p">,</span><span class="w"/><span class="s">"replace pantry item"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">MAPPER</span><span class="p">.</span><span class="na">readValue</span><span class="p">(</span><span class="n">resp</span><span class="p">.</span><span class="na">body</span><span class="p">(),</span><span class="w"/><span class="n">PantryItem</span><span class="p">.</span><span class="na">class</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">RuntimeException</span><span class="w"/><span class="n">re</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">throw</span><span class="w"/><span class="n">re</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">Exception</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">throw</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">RuntimeException</span><span class="p">(</span><span class="s">"Failed to replace pantry item "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">item</span><span class="p">.</span><span class="na">name</span><span class="p">(),</span><span class="w"/><span class="n">e</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>An<code>HttpRequest</code> with the method<code>PUT</code> carries the entry, cast as JSON, to the
address of the REST server; a check of the status code ensures success. Creating and
deleting proceed with<code>POST</code> and<code>DELETE</code> respectively, after the same pattern. No
additional dependency is needed for this; the platform&rsquo;s<code>HttpClient</code> suffices.</p><p>The profile section of the sidebar is built after the same pattern. Its write
transmits the changed profile through the associated REST client, and its refresh
likewise reloads the profile from the server into the form. What holds for the
pantry therefore holds for the dietary profile as well: write, then reload.</p><p>There remains the opposite direction. The writes considered so far proceed from the
operator. The second writer is the model, which changes the same state through the
MCP tools. So that its changes too become visible, the main view refreshes the
sidebar after every MCP run — those very two calls in the<code>finally</code> block that the
previous chapter announced: the reloading of the grid and the reloading of the
profile form. Thus the circle closes in both directions. If the operator changes the
data, the next MCP-enabled answer reflects the change, because the model re-fetches
the state through the tools on every request. If the model changes the data, the
sidebar reflects it, because it reloads after the run.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-diagram-06.png" alt="Two writers, one source of truth — operator and model write through to the REST server" loading="lazy" decoding="async"/><figure><img src="/images/2026/06/recipe-assistant-part-1-ch07.png" alt="Two writers, one source of truth: operator and model change the same state, while the display reloads after every change" loading="lazy" decoding="async"/><p>This very closed circle is the actual demonstration value of the sidebar. The
operator can show, without a restart, how a change of data affects the next answer —
for instance by switching the diet, adding an intolerance or striking out an
ingredient — and he can at the same time observe how a change carried out by the
model appears at once in the sidebar. The rigour with which everything is reloaded
after every change is the condition for this demonstration to succeed reliably. With
that, the constituents of the main view have been considered in full; the following
and final chapter of this part draws the sum.</p><h2 id="8-conclusion">8. Conclusion</h2><p>The introduction to this part presented the main view as a proof: the single image
of two answers set side by side replaces the assertion about the value of MCP with
immediate perception. The guiding observation was that the persuasive force of this
juxtaposition rests less on the logic of the answers than on the discipline of their
presentation. The tour through the concrete hurdles has made good on that
observation.</p><p>Six hurdles had to be taken. Running without a starter laid the foundation by wiring
the start-up by hand and thereby disclosing what a starter otherwise conceals.
Conducting two requests concurrently created the simultaneity and, through the
symmetric construction of both columns, the equality of conditions. Channelling a
character-by-character stream into the component tree overcame the separation between
the background thread and the server-side interface, by having<code>@Push</code> and<code>UI.access</code> work together. Rendering the stream as Markdown let the answer come into
being as a structured document before the viewer&rsquo;s eyes. Making the protocol visible
turned the hidden tool traffic into a legible, running timeline. And keeping the
state consistent ensured that, after every change, the display reflects the
authoritative state, whether the operator or the model has changed it.</p><p>However different these hurdles are, a common thought connects them. An interface
for model-driven tool use must not merely display results; it must keep the process
of their emergence comprehensible. Each of the decisions discussed serves precisely
this comprehensibility: the simultaneity, so that the difference between the answers
becomes undeniable; the progressive rendering, so that the answer comes into being
vividly; the timeline, so that the traffic does not remain hidden; the reloading, so
that the display does not deceive. It is this very comprehensibility that turns a
mere display into a proof.</p><p>It should not be passed over that, for all its disclosure, the main view conceals
something. It sets the result of two requests side by side, yet it lets the
mechanism that brings forth the right-hand answer vanish into the automatic run. The
running timeline hints at it but does not halt it. It is precisely here that the
second part begins. The step-by-step view reverses the relationship: it halts the
pipeline, makes each phase individually triggerable, and turns the otherwise
fleeting course into a legible, temporally ordered object. Where this part showed
that the attached server makes a difference, the next will show how that difference
comes about — the passage from the what to the how.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-ch08.png" alt="A map of the six hurdles: from the question through the six stations to an answer you can follow" loading="lazy" decoding="async"/><p>With that, the first part is concluded. The main view is no mere accessory to the
MCP server, but the means by which its value becomes perceptible — and it is so
precisely because it makes the discipline of its presentation its business.</p>
]]></content:encoded><category>Java</category><category>Vaadin</category><category>MCP</category><media:content url="https://svenruppert.com/images/2026/06/recipe-assistant-vaadin-ui-part-1-hero.jpg" medium="image"/><media:thumbnail url="https://svenruppert.com/images/2026/06/recipe-assistant-vaadin-ui-part-1-hero.jpg"/><enclosure url="https://svenruppert.com/images/2026/06/recipe-assistant-vaadin-ui-part-1-hero.jpg" type="image/jpeg" length="0"/></item></channel></rss>