<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tom Shaw</title>
    <description>The latest articles on DEV Community by Tom Shaw (@tomshaw).</description>
    <link>https://dev.to/tomshaw</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F163438%2Fc7eaaf28-f1f6-4c2c-8452-5544bf7723af.jpeg</url>
      <title>DEV Community: Tom Shaw</title>
      <link>https://dev.to/tomshaw</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tomshaw"/>
    <language>en</language>
    <item>
      <title>Wiring components to server-rendered pages with domwire</title>
      <dc:creator>Tom Shaw</dc:creator>
      <pubDate>Thu, 11 Jun 2026 17:21:38 +0000</pubDate>
      <link>https://dev.to/tomshaw/wiring-components-to-server-rendered-pages-with-domwire-1373</link>
      <guid>https://dev.to/tomshaw/wiring-components-to-server-rendered-pages-with-domwire-1373</guid>
      <description>&lt;p&gt;&lt;a href="https://www.npmjs.com/package/domwire" rel="noopener noreferrer"&gt;domwire&lt;/a&gt; is a DOM-driven, on-demand component loader for plain JavaScript and TypeScript applications. It initializes ES6 classes from &lt;code&gt;data-component&lt;/code&gt; attributes in your markup, manages their lifecycle, and lazy-loads their code only when the matching element actually exists on the page. It has zero runtime dependencies and weighs about 2 KB minified.&lt;/p&gt;

&lt;p&gt;This tutorial covers why the library exists, the problem it solves, how the approach works, and how to use every part of the API.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The problem
&lt;/h2&gt;

&lt;p&gt;Most web applications are not single-page apps. Server-rendered sites — Rails, Laravel, Django, WordPress, static sites — still need JavaScript behavior: a date picker here, a carousel there, an autocomplete on one form out of fifty pages. The question every one of these codebases has to answer is: &lt;strong&gt;how does a piece of JavaScript find out whether the page it's running on needs it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The answer you find in most codebases, including ones written by experienced developers, looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app.js — runs on every page&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;UserCard&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/UserCard.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Carousel&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/Carousel.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DatePicker&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/DatePicker.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Autocomplete&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/Autocomplete.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...30 more imports&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userCard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.user-card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userCard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userCard&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;carousel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.carousel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;carousel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Carousel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;carousel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.date-picker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DatePicker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Autocomplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minChars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// ...30 more of these&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sometimes it's dressed up as an array of &lt;code&gt;{ selector, klass }&lt;/code&gt; pairs with a loop over it, but it's the same pattern. Every component in the entire application interrogates every page: &lt;em&gt;"am I needed here?"&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this is fundamentally awkward
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The direction of the question is inverted.&lt;/strong&gt; The server already knows exactly which components a page needs — it rendered the markup. But that knowledge is thrown away, and the client re-derives it by checking every selector the application has ever defined against the current document. The page should &lt;em&gt;declare&lt;/em&gt; what it needs; instead, every script &lt;em&gt;asks&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every page pays for every component.&lt;/strong&gt; All 34 imports above are in the bundle whether the current page uses zero of them or all of them. The blog index downloads, parses, and executes the checkout form's validation logic. Code splitting is technically possible, but the manual pattern gives you no natural seam to split along.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The wiring file becomes a bottleneck.&lt;/strong&gt; Every new component means editing the same central file: add an import, add a selector check. The file grows monotonically, merge conflicts concentrate there, and nothing ever gets removed because nobody is sure which pages still use which selector.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration gets smuggled through ad-hoc channels.&lt;/strong&gt; One component reads &lt;code&gt;data-min-chars&lt;/code&gt;, another reads a global, another parses a class name like &lt;code&gt;carousel--speed-3&lt;/code&gt;. There is no convention because the pattern doesn't provide one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamically inserted markup silently does nothing.&lt;/strong&gt; The selector checks run once at &lt;code&gt;DOMContentLoaded&lt;/code&gt;. Content that arrives later — an htmx swap, a fetched partial, an infinite-scroll page — contains markup that looks right but is inert, because the check already ran. The usual fixes (re-running the whole init function, framework-specific re-init events) are fragile and re-initialize things that were already alive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teardown doesn't exist.&lt;/strong&gt; When a node holding a component is removed, its event listeners on &lt;code&gt;window&lt;/code&gt; or &lt;code&gt;document&lt;/code&gt;, its timers, and its observers keep running. The manual pattern has no place where destruction could even happen.&lt;/p&gt;

&lt;p&gt;None of these are exotic edge cases. They are the default failure modes of the pattern, and they show up in almost every non-framework codebase of meaningful size.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The idea behind domwire
&lt;/h2&gt;

&lt;p&gt;domwire inverts the question. Instead of every component asking the page "do you need me?", the page states what it needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-component=&lt;/span&gt;&lt;span class="s"&gt;"user-card"&lt;/span&gt; &lt;span class="na"&gt;data-options=&lt;/span&gt;&lt;span class="s"&gt;'{"id": 42}'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and a single manager walks the DOM once, looks up each declared name in a registry, loads the matching class, and instantiates it on that element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ComponentManager&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;domwire&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ComponentManager&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;UserCard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/UserCard.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;Carousel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/Carousel.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;DatePicker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/DatePicker.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire wiring for the whole application. Three things changed structurally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The markup is the manifest.&lt;/strong&gt; The server-side template that renders the element also declares its behavior, in the same place. The knowledge the server had is no longer discarded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The registry holds importers, not classes.&lt;/strong&gt; &lt;code&gt;() =&amp;gt; import(...)&lt;/code&gt; is a function that &lt;em&gt;hasn't run yet&lt;/em&gt;. A page with no &lt;code&gt;user-card&lt;/code&gt; element never downloads &lt;code&gt;UserCard.js&lt;/code&gt;. The bundler sees the dynamic import and splits each component into its own chunk automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is exactly one query.&lt;/strong&gt; One &lt;code&gt;querySelectorAll("[data-component]")&lt;/code&gt; per boot, instead of N selector checks for N components.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. Getting started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;domwire
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Write a component
&lt;/h3&gt;

&lt;p&gt;A component is a class extending &lt;code&gt;AbstractComponent&lt;/code&gt;. It receives the element it was declared on and the parsed options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/UserCard.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AbstractComponent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;domwire&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserCard&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`User &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// remove listeners, cancel timers, etc.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;this.el&lt;/code&gt; is the &lt;code&gt;HTMLElement&lt;/code&gt; carrying the &lt;code&gt;data-component&lt;/code&gt; attribute. &lt;code&gt;this.options&lt;/code&gt; is the object parsed from &lt;code&gt;data-options&lt;/code&gt;. The component must be the module's &lt;strong&gt;default export&lt;/strong&gt; — that is what the loader unwraps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Declare it in markup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-component=&lt;/span&gt;&lt;span class="s"&gt;"user-card"&lt;/span&gt; &lt;span class="na"&gt;data-options=&lt;/span&gt;&lt;span class="s"&gt;'{"id": 42}'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attribute value is kebab-case; the registry key is its PascalCase form (&lt;code&gt;user-card&lt;/code&gt; → &lt;code&gt;UserCard&lt;/code&gt;). This keeps the HTML idiomatic (HTML is case-insensitive, so PascalCase attributes are unreliable) while the registry keys match your class and file names.&lt;/p&gt;

&lt;h3&gt;
  
  
  Boot
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ComponentManager&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;domwire&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ComponentManager&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;UserCard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/UserCard.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;boot()&lt;/code&gt; queries the document for &lt;code&gt;[data-component]&lt;/code&gt;, and for each match: parses the options, loads the class through the registry, instantiates it, and runs the init lifecycle. All elements initialize concurrently — &lt;code&gt;boot()&lt;/code&gt; resolves when every component on the page is mounted.&lt;/p&gt;

&lt;p&gt;You can boot a subtree instead of the whole document by passing a root: &lt;code&gt;manager.boot(someContainer)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Passing options
&lt;/h2&gt;

&lt;p&gt;Options travel as JSON in &lt;code&gt;data-options&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
    &lt;span class="na"&gt;data-component=&lt;/span&gt;&lt;span class="s"&gt;"autocomplete"&lt;/span&gt;
    &lt;span class="na"&gt;data-options=&lt;/span&gt;&lt;span class="s"&gt;'{"minChars": 3, "endpoint": "/api/search", "limit": 10}'&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Autocomplete&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;minChars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This replaces every ad-hoc configuration channel — per-attribute reads, globals, encoded class names — with one convention. Server templates produce it trivially:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-component=&lt;/span&gt;&lt;span class="s"&gt;"autocomplete"&lt;/span&gt;
     &lt;span class="na"&gt;data-options=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;minChars: &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;endpoint: &lt;/span&gt;&lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Invalid JSON does not break the page: domwire logs a warning and the component receives &lt;code&gt;{}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In TypeScript, options are typed via the generic parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AutocompleteOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;minChars&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Autocomplete&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AutocompleteOptions&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// string — typed&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. The lifecycle
&lt;/h2&gt;

&lt;p&gt;Each instance runs through six hooks. On initialization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;beforeCreate → created → beforeMount → mounted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and on teardown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;beforeDestroy → destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All hooks are optional — implement only the ones you need. In practice, &lt;code&gt;mounted&lt;/code&gt; is where setup belongs (listeners, initial render, fetches) and &lt;code&gt;destroy&lt;/code&gt; is where cleanup belongs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Dropdown&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onDocClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onDocClick&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onDocClick&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The teardown hooks are what the manual pattern structurally lacks. Because the manager tracks every instance in a &lt;code&gt;Map&amp;lt;HTMLElement, AbstractComponent&amp;gt;&lt;/code&gt;, it can tear components down deterministically — either when you call &lt;code&gt;destroyComponent(el)&lt;/code&gt; / &lt;code&gt;destroyAll()&lt;/code&gt;, or automatically when observed nodes leave the DOM (next section).&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Dynamic content: &lt;code&gt;observe()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Pages that mutate after load — htmx or Turbo swaps, fetched partials, modals injected on demand, infinite scroll — are where the manual pattern breaks down hardest. domwire handles them with a &lt;code&gt;MutationObserver&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From that point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any inserted node matching &lt;code&gt;[data-component]&lt;/code&gt; (or containing matches in its subtree) is initialized automatically, through the same registry, options parsing, and lifecycle as &lt;code&gt;boot()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Any removed node holding an instance (or containing instances) gets &lt;code&gt;beforeDestroy → destroy&lt;/code&gt; and is dropped from the instance map.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means a server can return a fragment like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-component=&lt;/span&gt;&lt;span class="s"&gt;"chart"&lt;/span&gt; &lt;span class="na"&gt;data-options=&lt;/span&gt;&lt;span class="s"&gt;'{"series": [3, 5, 8]}'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;from any endpoint, a library like htmx can swap it in, and it simply works — no re-init call, no custom events, no coupling between the swapping mechanism and the component system. Call &lt;code&gt;manager.unobserve()&lt;/code&gt; to stop watching.&lt;/p&gt;

&lt;p&gt;The destroy side is just as important: components removed by a swap release their listeners and timers instead of leaking. The combination — declarative init plus automatic teardown — is what makes long-lived, partially-updating pages safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Lazy loading in depth
&lt;/h2&gt;

&lt;p&gt;This is the part that changes your bundle profile, so it deserves a closer look.&lt;/p&gt;

&lt;p&gt;A registry entry is an &lt;em&gt;importer&lt;/em&gt;: a function returning a promise of a module with a default export.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;UserCard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/UserCard.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three properties fall out of this shape:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code splitting is automatic.&lt;/strong&gt; Bundlers (Vite, webpack, Rollup, esbuild) treat each dynamic &lt;code&gt;import()&lt;/code&gt; as a split point. Each component becomes its own chunk without any bundler configuration. Your entry bundle contains domwire (~2 KB) plus the registry — a map of names to thin importer functions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loading is demand-driven by the DOM itself.&lt;/strong&gt; The importer only runs when an element declaring that component exists. The cost model becomes exact: a page pays for precisely the components it renders, nothing more. A heavy chart library behind a &lt;code&gt;chart&lt;/code&gt; component costs zero bytes on every page without a chart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eager loading remains available where it's the right call.&lt;/strong&gt; For a tiny component used on every page, skip the network round-trip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;SiteNav&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/SiteNav.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nl"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;SiteNav&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SiteNav&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="c1"&gt;// eager, in main bundle&lt;/span&gt;
    &lt;span class="na"&gt;Chart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/Chart.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;       &lt;span class="c1"&gt;// lazy, own chunk&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The registry is also the single audit point the manual pattern never had: one object that lists every component the application can instantiate. Components missing from it trigger the &lt;code&gt;onMissing&lt;/code&gt; callback rather than failing silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Namespaces
&lt;/h2&gt;

&lt;p&gt;Larger applications can scope registry keys by passing &lt;code&gt;namespace&lt;/code&gt; in the options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-component=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt; &lt;span class="na"&gt;data-options=&lt;/span&gt;&lt;span class="s"&gt;'{"namespace": "admin"}'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;resolves to the registry key &lt;code&gt;admin/Card&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin/Card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./admin/components/Card.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/Card.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets independent sections of an application (or separately-owned bundles) use the same component names without collision.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Error handling
&lt;/h2&gt;

&lt;p&gt;Two callbacks cover the failure paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ComponentManager&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;onMissing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// a data-component name with no registry entry&lt;/span&gt;
        &lt;span class="nf"&gt;reportToMonitoring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`unknown component: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// the component constructor threw&lt;/span&gt;
        &lt;span class="nf"&gt;reportToMonitoring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The defaults log to the console. A failing component never takes down the boot — every element initializes independently, so one broken widget leaves the rest of the page functional. This isolation is another thing the manual pattern lacks: in a single linear init function, one thrown error aborts everything after it unless every block is individually wrapped in try/catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Accessing instances and interop
&lt;/h2&gt;

&lt;p&gt;After initialization, the instance is reachable two ways:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// from the manager's Map&lt;/span&gt;
&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// directly from the element&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;el._component&lt;/code&gt; back-reference makes the system interoperable with anything else that can find a DOM element — event handlers, other scripts, the browser console (&lt;code&gt;$0._component&lt;/code&gt; while inspecting), and Alpine.js.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alpine.js integration
&lt;/h3&gt;

&lt;p&gt;If you use Alpine for light templating alongside domwire components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;registerAlpineMagic&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;domwire/alpine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;registerAlpineMagic&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This registers a &lt;code&gt;$component&lt;/code&gt; magic that resolves to the nearest &lt;code&gt;[data-component]&lt;/code&gt; ancestor's instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-component=&lt;/span&gt;&lt;span class="s"&gt;"cart"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;x-data&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"$component.addItem(7)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Add&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alpine handles the inline reactivity; the domwire class holds the real logic and state.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Why this approach
&lt;/h2&gt;

&lt;p&gt;It is worth being precise about why this design holds up against the alternatives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Against the manual selector-check pattern
&lt;/h3&gt;

&lt;p&gt;Every deficiency listed in section 1 maps to a structural property of domwire:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Manual pattern&lt;/th&gt;
&lt;th&gt;domwire&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Every component checks every page&lt;/td&gt;
&lt;td&gt;The page declares its components; one DOM query total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All component code in every bundle&lt;/td&gt;
&lt;td&gt;Importers load per-component, per-page, on demand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Central wiring file edited for every component&lt;/td&gt;
&lt;td&gt;Markup is the wiring; the registry is a flat declarative map&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ad-hoc configuration channels&lt;/td&gt;
&lt;td&gt;One convention: JSON in &lt;code&gt;data-options&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic content is inert&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;observe()&lt;/code&gt; initializes and destroys automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No teardown anywhere&lt;/td&gt;
&lt;td&gt;Tracked instances, lifecycle hooks, automatic destroy on removal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One error aborts the init sequence&lt;/td&gt;
&lt;td&gt;Per-element isolation with &lt;code&gt;onError&lt;/code&gt;/&lt;code&gt;onMissing&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key conceptual shift is &lt;strong&gt;direction&lt;/strong&gt;: control flows from the document to the code. The document is already the one artifact that accurately describes the page — the server built it. Treating it as the source of truth removes an entire category of synchronization problems, because there is nothing to keep in sync.&lt;/p&gt;

&lt;h3&gt;
  
  
  Against adopting a framework
&lt;/h3&gt;

&lt;p&gt;React, Vue, or Svelte solve this problem too — but by owning the DOM. For a server-rendered application, that means either rewriting rendering in the framework or maintaining hydration boundaries, build integration, and a client-side rendering model the application didn't need. domwire's scope is deliberately narrow: it connects classes to existing server-rendered elements. It doesn't render, doesn't manage state, doesn't own the DOM. Your components are plain ES6 classes that do ordinary DOM work, and the entire abstraction is small enough to read in one sitting (~230 lines of source).&lt;/p&gt;

&lt;h3&gt;
  
  
  Against Web Components / custom elements
&lt;/h3&gt;

&lt;p&gt;Custom elements (&lt;code&gt;&amp;lt;user-card&amp;gt;&lt;/code&gt;) offer browser-native lifecycle and are a reasonable alternative. The trade-offs that favor &lt;code&gt;data-component&lt;/code&gt; attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Behavior attaches to &lt;em&gt;existing&lt;/em&gt; semantic elements — a &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;, a &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;, a &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; — without wrapping them in a new tag or fighting CSS expectations around unknown elements.&lt;/li&gt;
&lt;li&gt;Lazy loading must be hand-built around &lt;code&gt;customElements.define&lt;/code&gt; (you need a separate lazy-definition mechanism); in domwire it is the default shape of the registry.&lt;/li&gt;
&lt;li&gt;Options pass as one typed JSON object rather than string attributes parsed one at a time.&lt;/li&gt;
&lt;li&gt;No shadow DOM implications, no constructor restrictions (custom element constructors can't touch attributes or children), no upgrade-timing subtleties.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Custom elements shine for distributable, encapsulated widgets. For wiring application behavior onto server-rendered pages, the attribute-plus-registry model is simpler and more direct.&lt;/p&gt;

&lt;h3&gt;
  
  
  Against jQuery-style plugins
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;$(".date-picker").datepicker()&lt;/code&gt; is the manual pattern with different syntax — the selector checks, the all-pages bundle, the missing teardown, and the inert dynamic content all carry over unchanged.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost
&lt;/h3&gt;

&lt;p&gt;Honest accounting: you take on one attribute convention in your markup, a registry file, and a ~2 KB runtime. Component code itself is unchanged — they are classes that receive an element, which is what you would have written anyway. There is no compiler, no build-step requirement beyond what your bundler already does, and no lock-in: removing domwire means replacing &lt;code&gt;boot()&lt;/code&gt; with the manual wiring it eliminated.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. API reference
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ComponentManager&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ComponentManager&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// default "[data-component]"&lt;/span&gt;
    &lt;span class="nl"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ComponentRegistry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ComponentLoader&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// supply your own resolution strategy&lt;/span&gt;
    &lt;span class="nl"&gt;onMissing&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;el&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;el&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Member&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;boot(root?)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Initialize all matching elements under &lt;code&gt;root&lt;/code&gt; (default: &lt;code&gt;document&lt;/code&gt;). Resolves when all are done.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;observe(root?)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Watch for added/removed matching nodes; init and destroy automatically.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unobserve()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stop watching.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;initializeComponent(el)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Initialize a single element manually.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;destroyComponent(el)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run teardown hooks and forget the instance.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;destroyAll()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tear down every tracked instance.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;instances&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Map&amp;lt;HTMLElement, AbstractComponent&amp;gt;&lt;/code&gt; of live instances.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;AbstractComponent&amp;lt;TOptions, TElement&amp;gt;&lt;/code&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Member&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;el&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The host element.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;options&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parsed &lt;code&gt;data-options&lt;/code&gt; object.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;beforeCreate / created / beforeMount / mounted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Init hooks, called in that order.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;beforeDestroy / destroy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Teardown hooks.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ComponentLoader&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;new ComponentLoader(registry)&lt;/code&gt; — resolves kebab-case names (optionally namespaced) to constructors via the registry. Returns &lt;code&gt;null&lt;/code&gt; for unknown names or failed imports. Used internally by &lt;code&gt;ComponentManager&lt;/code&gt;; injectable for custom resolution.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;registerAlpineMagic(options?)&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;From &lt;code&gt;domwire/alpine&lt;/code&gt;. Registers an Alpine magic (default name &lt;code&gt;component&lt;/code&gt;, default selector &lt;code&gt;[data-component]&lt;/code&gt;) resolving to the nearest instance.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. Summary
&lt;/h2&gt;

&lt;p&gt;The selector-check init file is one of the most common pieces of accidental architecture in web development: every component asking every page whether it's needed, every page shipping every component, and nothing handling content that arrives late or leaves early. domwire replaces it by letting the markup — the one artifact that already knows what the page contains — drive initialization. One attribute, one registry, one boot call; lazy loading, lifecycle, dynamic-content handling, and error isolation follow from the structure rather than from discipline.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a Laravel MCP Server That Answers Questions Over Real Data</title>
      <dc:creator>Tom Shaw</dc:creator>
      <pubDate>Thu, 11 Jun 2026 06:59:54 +0000</pubDate>
      <link>https://dev.to/tomshaw/building-a-laravel-mcp-server-that-answers-questions-over-real-data-3cjg</link>
      <guid>https://dev.to/tomshaw/building-a-laravel-mcp-server-that-answers-questions-over-real-data-3cjg</guid>
      <description>&lt;h2&gt;
  
  
  Building a Laravel MCP Server That Answers Questions Over Real Data
&lt;/h2&gt;

&lt;h2&gt;
  
  
  A hands-on guide to the Model Context Protocol in Laravel — from first principles to a live tool inside Claude Desktop
&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol (MCP) lets an AI assistant like Claude &lt;em&gt;call into your application&lt;/em&gt; — running your code, reading your data, and answering questions it otherwise couldn't. This tutorial teaches MCP in Laravel by building a working example: a &lt;strong&gt;read-only server that answers natural-language questions over a sales database&lt;/strong&gt; ("who were our top five customers last quarter?") and connecting it to Claude Desktop.&lt;/p&gt;

&lt;p&gt;We'll use a sample sales dataset — customers, orders, products, payments — based on the well-known &lt;a href="https://www.mysqltutorial.org/mysql-sample-database.aspx" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;classicmodels&lt;/code&gt;&lt;/strong&gt; sample database&lt;/a&gt; (a scale-model car retailer), popularised by MySQL Tutorial and freely available for learning. Nothing here is specific to it, though; the same patterns apply to any Laravel application's data. Every code snippet is an illustrative example; the complete, runnable server is in the accompanying project, linked at the end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll learn&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What MCP is&lt;/strong&gt; and how it differs from a traditional REST API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to test a server locally&lt;/strong&gt; with the MCP Inspector, before any AI client touches it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to make your tools "agent-native"&lt;/strong&gt; — the small touches that make a server pleasant for a model to use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to connect the server to Claude Desktop&lt;/strong&gt;, and how to secure it for real use.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Part 1 — What is MCP, and how is it different from an API?
&lt;/h2&gt;

&lt;p&gt;An MCP server is, in one sense, &lt;em&gt;an API&lt;/em&gt; — it speaks JSON-RPC over a transport and returns structured data. But it's a fundamentally different &lt;strong&gt;kind&lt;/strong&gt; of interface than the REST API you'd hand-write for a single-page app or a mobile client.&lt;/p&gt;

&lt;h3&gt;
  
  
  The core difference: who the consumer is
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A REST API is designed for developers.&lt;/strong&gt; A human reads the docs, learns the endpoints, and writes code to call &lt;code&gt;GET /orders?status=Shipped&lt;/code&gt;. The contract lives in documentation that sits &lt;em&gt;outside&lt;/em&gt; the API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An MCP server is designed for an AI model.&lt;/strong&gt; The model discovers what's available &lt;em&gt;at runtime&lt;/em&gt; by asking the server "what can you do?" The contract is &lt;strong&gt;self-describing&lt;/strong&gt; and ships &lt;em&gt;with&lt;/em&gt; the server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That single shift drives every other difference.&lt;/p&gt;

&lt;h3&gt;
  
  
  Side by side
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;REST API&lt;/th&gt;
&lt;th&gt;MCP Server&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Consumer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A developer writing code&lt;/td&gt;
&lt;td&gt;An AI model, at runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Discovery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read external docs&lt;/td&gt;
&lt;td&gt;The server advertises its tools + schemas itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docs say &lt;em&gt;how to call it&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Each tool says &lt;em&gt;what it does and when to use it&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coupling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Client hard-codes endpoints&lt;/td&gt;
&lt;td&gt;Client asks "what can you do?" and adapts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primitives&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Endpoints + HTTP verbs&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Tools&lt;/strong&gt; (actions), &lt;strong&gt;Resources&lt;/strong&gt; (data), &lt;strong&gt;Prompts&lt;/strong&gt; (templates)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transport/auth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HTTP, you design it&lt;/td&gt;
&lt;td&gt;Standardized (stdio / streamable HTTP) — any MCP client connects&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The three primitives
&lt;/h3&gt;

&lt;p&gt;A Laravel MCP server is a class that registers three kinds of capability. Here's the skeleton of the server we'll build, which exposes all three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Mcp\Server&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Mcp\Server\Attributes\Instructions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Instructions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;&amp;lt;&amp;lt;&amp;lt;'TXT'
    This server exposes a read-only sales dataset for ad-hoc querying.
    Read the "database-schema" resource first to learn the tables, then
    answer with the curated tools, falling back to a read-only SQL query
    only when no curated tool fits.
    TXT&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AssistantServer&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Server&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;ListOrdersTool&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;SearchCustomersTool&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;RevenueReportTool&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;RunAssistantQueryTool&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;DatabaseSchemaResource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$prompts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;RevenueInsightsPrompt&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt; are the callable actions — &lt;code&gt;ListOrdersTool&lt;/code&gt;, &lt;code&gt;SearchCustomersTool&lt;/code&gt;, and so on. Each is a class with a &lt;code&gt;handle()&lt;/code&gt; method, much like a controller action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources&lt;/strong&gt; are read-only context the model can pull in. Our &lt;code&gt;DatabaseSchemaResource&lt;/code&gt; describes every queryable table and column in plain language, so the model knows the shape of the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompts&lt;/strong&gt; are reusable templates — &lt;code&gt;RevenueInsightsPrompt&lt;/code&gt; seeds a canned analysis workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice the &lt;code&gt;#[Instructions]&lt;/code&gt; block. It tells the model &lt;em&gt;how to work&lt;/em&gt; — read the schema first, prefer curated tools, treat raw SQL as a fallback — and the model receives it automatically on connect. A REST API has no equivalent; the closest thing is a README the developer might never read.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Self-describing &amp;amp; runtime-discoverable.&lt;/strong&gt; A tool's description ("Lists orders with their status, dates, customer and computed total…") is part of the wire protocol. The model reads it and knows &lt;em&gt;when&lt;/em&gt; to reach for the tool — no human gluing docs to code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One client, any server.&lt;/strong&gt; Because MCP is a standard, Claude Desktop connects to your Laravel server with zero bespoke integration code. Contrast a REST API, where every consumer writes its own client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built for reasoning, not just transport.&lt;/strong&gt; Descriptions and JSON schemas are written to help a model &lt;em&gt;decide&lt;/em&gt;. That's a different design goal than an endpoint that just returns rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You keep all your Laravel guts.&lt;/strong&gt; Tools run inside the app, so they reuse Eloquent, validation, the query builder, config, and auth. You expose your domain to a model without rebuilding it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The honest trade-offs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not for browsers or mobile apps.&lt;/strong&gt; If a human-facing frontend needs the data, a REST/JSON API is still the right tool. MCP targets AI clients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More machinery for trivial cases.&lt;/strong&gt; If you just need one endpoint hit by your own SPA, MCP is overkill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Younger ecosystem.&lt;/strong&gt; REST tooling (gateways, caching, doc generators) is mature; MCP is newer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The mental model
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;A REST API is a &lt;strong&gt;menu you read&lt;/strong&gt;. An MCP server is a &lt;strong&gt;waiter&lt;/strong&gt; who tells you the specials, knows what you can order, and places it for you. Same kitchen (your Laravel app) — a different way of interacting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  A word on data access (read this before you connect anything)
&lt;/h3&gt;

&lt;p&gt;A common worry is &lt;em&gt;"does the model connect to my database?"&lt;/em&gt; It does not — it has no connection, no credentials, and can't reach your database at all. The model only ever &lt;strong&gt;emits a tool call&lt;/strong&gt; and &lt;strong&gt;reads the JSON your tool returns&lt;/strong&gt;; your Laravel app is the one holding the connection and running the query.&lt;/p&gt;

&lt;p&gt;But here's the part to internalise: &lt;strong&gt;for the assistant to answer, your tools must read real rows and return them — and those rows land in the model's context.&lt;/strong&gt; So the exposure isn't the connection, it's the &lt;em&gt;return values&lt;/em&gt;. Any data your tools can read can end up in a conversation, travel to the model provider's API, and be shown to whoever is using the AI client. That reframes what a server's guardrails are really protecting:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Looks like it's for&lt;/th&gt;
&lt;th&gt;What it's &lt;em&gt;actually&lt;/em&gt; protecting&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read-only DB connection&lt;/td&gt;
&lt;td&gt;Preventing writes&lt;/td&gt;
&lt;td&gt;The blast radius if a guard is ever bypassed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table allowlist&lt;/td&gt;
&lt;td&gt;Stopping bad SQL&lt;/td&gt;
&lt;td&gt;Deciding what data is allowed to leave the building&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blocked column patterns&lt;/td&gt;
&lt;td&gt;SQL hygiene&lt;/td&gt;
&lt;td&gt;Keeping credentials/PII out of the model's context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Curated tools selecting set columns&lt;/td&gt;
&lt;td&gt;Clean output&lt;/td&gt;
&lt;td&gt;Not returning columns the model never needs to see&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The design principle: &lt;strong&gt;scope read access to exactly the data you're willing to put in front of a model.&lt;/strong&gt; In practice that means a dedicated read-only database user, treating every returned row as data that leaves your perimeter, and authenticating who can reach the server. We'll act on all three in Part 4. The one-liner to remember:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The app needs read access; whatever it reads and returns becomes visible to the model and the user. Scope the read access to what you're willing to expose, and authenticate who can ask.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With the &lt;em&gt;what&lt;/em&gt; and the &lt;em&gt;why&lt;/em&gt; settled, let's get a server running and prove it works — before any AI client is involved.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2 — Test the server locally with the MCP Inspector
&lt;/h2&gt;

&lt;p&gt;Laravel MCP ships an &lt;strong&gt;Inspector&lt;/strong&gt;: a local web UI that connects to your server and lets you list and call tools, read resources, and watch the raw JSON-RPC traffic. It's the fastest debug loop you have, and the right place to live before you ever open an AI client. First, though, the server has to be reachable.&lt;/p&gt;

&lt;h3&gt;
  
  
  How a server is exposed
&lt;/h3&gt;

&lt;p&gt;You register a server on a route in &lt;code&gt;routes/ai.php&lt;/code&gt;. Our example mounts it as a &lt;strong&gt;web&lt;/strong&gt; (HTTP) server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Mcp\Servers\AssistantServer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Mcp\Facades\Mcp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Mcp&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;web&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/mcp/assistant'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;AssistantServer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'throttle:mcp'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That exposes a streamable-HTTP endpoint at &lt;code&gt;/mcp/assistant&lt;/code&gt;. The &lt;code&gt;throttle:mcp&lt;/code&gt; rate limiter is a plain Laravel limiter — in the example it allows 60 requests/min for an authenticated user, or 20/min by IP otherwise, so the endpoint is reachable in local dev without a token.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Keep the route unauthenticated while you test.&lt;/strong&gt; The Inspector connects to a web server over HTTP, through the &lt;em&gt;real route&lt;/em&gt; — so any auth middleware on it applies. An &lt;code&gt;auth&lt;/code&gt; or &lt;code&gt;auth:sanctum&lt;/code&gt; guard will reject the Inspector's unauthenticated connection. Register the route with &lt;code&gt;throttle:mcp&lt;/code&gt; only for now; we add authentication in Part 4, once the server is ready to expose.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Launch the Inspector
&lt;/h3&gt;

&lt;p&gt;Make sure your app is being served (&lt;code&gt;php artisan serve&lt;/code&gt;), then point the Inspector at the server's &lt;strong&gt;route or handle&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan mcp:inspector mcp/assistant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This launches the MCP Inspector and opens it in your browser, connected to your running endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  A testing checklist
&lt;/h3&gt;

&lt;p&gt;Work through the server methodically — each step mirrors something the model will do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;List the tools.&lt;/strong&gt; Confirm they all appear with readable names and descriptions. This is exactly what a model sees on connect — if a description is vague &lt;em&gt;here&lt;/em&gt;, the model will misuse the tool &lt;em&gt;there&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the schema resource.&lt;/strong&gt; It should return your curated table/column documentation. The server's instructions tell the model to read this first, so make sure it's clear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call a curated tool.&lt;/strong&gt; Run &lt;code&gt;list-orders&lt;/code&gt; with &lt;code&gt;{ "status": "Shipped", "limit": 5 }&lt;/code&gt;. You should get back JSON with the matching rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exercise validation.&lt;/strong&gt; Call &lt;code&gt;list-orders&lt;/code&gt; with &lt;code&gt;{ "status": "Banana" }&lt;/code&gt;. You should get a friendly error like &lt;em&gt;"Invalid status. Choose one of: In Process, Shipped, …"&lt;/em&gt;. Good error text is how a model self-corrects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the raw-SQL guardrails.&lt;/strong&gt; If your server has an open-ended query tool, call it with a write attempt (&lt;code&gt;DELETE FROM orders&lt;/code&gt;) and confirm it's rejected, then a valid &lt;code&gt;SELECT&lt;/code&gt; and confirm it runs, then a forbidden table (&lt;code&gt;SELECT * FROM users&lt;/code&gt;) and confirm the allowlist blocks it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try a prompt.&lt;/strong&gt; Invoke one and confirm it returns a sensible starter message.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If every item passes, the server is sound.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; keep the Inspector open in a tab while you edit tools. After a change, just re-run a tool call — a far tighter loop than restarting an AI client each time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your tools &lt;em&gt;work&lt;/em&gt;. But "works" and "pleasant for a model to use" aren't the same thing — and the gap between them is where most of the real craft of an MCP server lives. That's Part 3.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3 — Make the tools "agent-native"
&lt;/h2&gt;

&lt;p&gt;A tool can return correct data and still be hard for a model to use well. The refinements below don't change &lt;em&gt;what&lt;/em&gt; a tool does; they change how much the model &lt;em&gt;understands&lt;/em&gt; about each call without trial and error — Is it safe to retry? What shape comes back? Did I get all the data? That understanding is the whole "designed for agents" difference. We'll show each as a small before/after.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Tool annotations
&lt;/h3&gt;

&lt;p&gt;A read-only tool should &lt;em&gt;say&lt;/em&gt; it's read-only. Annotations are metadata the host (the AI client) uses to decide whether a call needs user confirmation, can be safely retried, cached, or run in parallel. Without them, a host has to treat every call as potentially dangerous.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Mcp\Server\Tools\Annotations\IsReadOnly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Mcp\Server\Tools\Annotations\IsIdempotent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Mcp\Server\Attributes\Description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[IsReadOnly]&lt;/span&gt;
&lt;span class="na"&gt;#[IsIdempotent]&lt;/span&gt;
&lt;span class="na"&gt;#[Description('Lists orders with their status, dates, customer and computed order total...')]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ListOrdersTool&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Tool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;#[IsReadOnly]&lt;/code&gt; and &lt;code&gt;#[IsIdempotent]&lt;/code&gt; to every read-only tool. For an open-ended SQL tool, add &lt;code&gt;#[IsOpenWorld(false)]&lt;/code&gt; to signal it never reaches outside your dataset. This is the cheapest change with the highest signal.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Annotation&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#[IsReadOnly]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The tool does not modify its environment.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#[IsDestructive]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The tool may perform destructive updates (only meaningful when &lt;em&gt;not&lt;/em&gt; read-only).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#[IsIdempotent]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Repeated calls with the same arguments have no additional effect.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#[IsOpenWorld]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The tool may interact with external entities.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  2. Structured responses + output schemas
&lt;/h3&gt;

&lt;p&gt;A tool that returns &lt;code&gt;Response::text(json_encode($rows))&lt;/code&gt; hands the model a &lt;strong&gt;text blob it has to re-parse&lt;/strong&gt;, and the model can't know the result's shape until &lt;em&gt;after&lt;/em&gt; it calls. &lt;code&gt;laravel/mcp&lt;/code&gt; has first-class support for structured content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before:&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'row_count'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$rows&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'rows'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="no"&gt;JSON_PRETTY_PRINT&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// After:&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;structured&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'row_count'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$rows&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'rows'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then declare the shape with an &lt;code&gt;outputSchema()&lt;/code&gt; method, so the model knows what it'll get back &lt;em&gt;before&lt;/em&gt; it calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Contracts\JsonSchema\JsonSchema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;outputSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;JsonSchema&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'row_count'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="s1"&gt;'rows'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Matching order rows.'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Response::structured()&lt;/code&gt; keeps a JSON-encoded text fallback for older clients, so you lose nothing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on return types.&lt;/strong&gt; &lt;code&gt;Response::structured()&lt;/code&gt; returns a &lt;code&gt;ResponseFactory&lt;/code&gt;, not a &lt;code&gt;Response&lt;/code&gt;, so widen the method signature to &lt;code&gt;handle(Request $request): Response|ResponseFactory&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha — declare nullable fields, or the whole response is rejected.&lt;/strong&gt; The client doesn't just &lt;em&gt;read&lt;/em&gt; your &lt;code&gt;outputSchema&lt;/code&gt;, it &lt;strong&gt;validates the response against it&lt;/strong&gt;. If a field can be &lt;code&gt;null&lt;/code&gt; — say an optional date you echo back when no range was passed — but you declared it &lt;code&gt;$schema-&amp;gt;string()&lt;/code&gt;, then &lt;code&gt;null&lt;/code&gt; violates &lt;code&gt;"type": "string"&lt;/code&gt; and a strict client (Claude Desktop) throws the &lt;em&gt;entire&lt;/em&gt; response away with an opaque &lt;strong&gt;"Tool execution failed"&lt;/strong&gt;. Your server never errors and your logs stay empty, because the rejection happens client-side. Mark such fields nullable:&lt;/p&gt;


&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'start_date'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Start date applied, or null.'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Telltale sign: the tool works over &lt;code&gt;php artisan mcp:start&lt;/code&gt; (the raw client doesn't validate) but fails in Claude Desktop. Inspect exactly what the client sees with &lt;code&gt;(new YourTool)-&amp;gt;toArray()['outputSchema']&lt;/code&gt; and check every field whose value can be null.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Drop &lt;code&gt;JSON_PRETTY_PRINT&lt;/code&gt; — token economy
&lt;/h3&gt;

&lt;p&gt;Models pay per token, and pretty-printing pads every response with whitespace the model doesn't need. &lt;code&gt;Response::structured()&lt;/code&gt; handles encoding for you, so this disappears the moment you adopt #2 — but it's worth stating as its own principle: &lt;strong&gt;for a model, terse beats pretty.&lt;/strong&gt; The same applies to any resource that hand-encodes JSON.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Signal truncation — a real design gap
&lt;/h3&gt;

&lt;p&gt;Capping a result set (&lt;code&gt;LIMIT 50&lt;/code&gt;, a &lt;code&gt;clampLimit()&lt;/code&gt; helper) silently hides rows. If 50 come back, the model &lt;strong&gt;can't tell&lt;/strong&gt; whether that's the whole answer or a wall it hit — so it may reason on incomplete data. Fetch one extra row to detect the cap, and tell the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;clampLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$validated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'limit'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$limit&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;    &lt;span class="c1"&gt;// one extra to detect "more"&lt;/span&gt;
&lt;span class="nv"&gt;$truncated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$rows&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$limit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;structured&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'row_count'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rows&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$limit&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'truncated'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$truncated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="c1"&gt;// model now knows to narrow filters or page&lt;/span&gt;
    &lt;span class="s1"&gt;'rows'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$rows&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Use real schema &lt;code&gt;-&amp;gt;default()&lt;/code&gt; values
&lt;/h3&gt;

&lt;p&gt;Documenting "(default 50)" in prose leaves the value in text the model has to interpret. &lt;code&gt;-&amp;gt;default(50)&lt;/code&gt; is machine-readable — something the client can act on directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'limit'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Maximum number of orders to return.'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Gate your highest-risk tool
&lt;/h3&gt;

&lt;p&gt;An open-ended SQL tool is the one surface where the guardrails &lt;em&gt;are&lt;/em&gt; the security boundary. You can expose it only to authorized users with &lt;code&gt;shouldRegister()&lt;/code&gt;, which runs per request — a tool that returns &lt;code&gt;false&lt;/code&gt; never appears in the tool list and can't be invoked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;shouldRegister&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;?-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'run-raw-queries'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(This requires an authenticated route and a matching ability — see Part 4.)&lt;/p&gt;

&lt;h3&gt;
  
  
  The takeaway
&lt;/h3&gt;

&lt;p&gt;None of this changes &lt;em&gt;what&lt;/em&gt; the server does. It changes how much the model understands about each call — read-only? retry-safe? what shape comes back? did I get everything? — without finding out the hard way. Items #1–#4 are the high-value set.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Test every change.&lt;/strong&gt; Each refinement is easy to cover: assert the new &lt;code&gt;truncated&lt;/code&gt; key, assert the structured output, assert a tool carries its annotations. A server you can't test, you can't trust in front of a model.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your server is correct &lt;em&gt;and&lt;/em&gt; agent-friendly. Time to put it in front of Claude.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4 — Connect the server to Claude Desktop
&lt;/h2&gt;

&lt;p&gt;Claude Desktop launches MCP servers as local processes that speak over stdio. Our server is a &lt;strong&gt;web&lt;/strong&gt; (HTTP) server, so we bridge the two with &lt;a href="https://www.npmjs.com/package/mcp-remote" rel="noopener noreferrer"&gt;&lt;code&gt;mcp-remote&lt;/code&gt;&lt;/a&gt; — a tiny npm tool that proxies Claude Desktop's stdio to an HTTP endpoint. No code change required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Start the server yourself, and keep it running
&lt;/h3&gt;

&lt;p&gt;This is the step that trips people up. With the web + &lt;code&gt;mcp-remote&lt;/code&gt; setup, Claude Desktop only launches the &lt;strong&gt;bridge&lt;/strong&gt; — it does &lt;strong&gt;not&lt;/strong&gt; start your Laravel app. So you have to run the server manually and leave it running for the whole session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan serve     &lt;span class="c"&gt;# or your usual dev command (composer run dev, Sail, Herd, etc.)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm the endpoint is live (e.g. &lt;code&gt;http://localhost:8000/mcp/assistant&lt;/code&gt;). If the app isn't running, the bridge connects to a dead URL and the tools never appear in Claude.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why manual, when other MCP servers "just start"?&lt;/strong&gt; A server registered with &lt;code&gt;Mcp::local(...)&lt;/code&gt; is launched by Claude Desktop itself — the config runs &lt;code&gt;php artisan mcp:start &amp;lt;handle&amp;gt;&lt;/code&gt; over stdio, so there's nothing to start by hand. A &lt;strong&gt;web&lt;/strong&gt; server (&lt;code&gt;Mcp::web&lt;/code&gt;) is the opposite: it's a normal HTTP endpoint that has to be up &lt;em&gt;before&lt;/em&gt; the client connects. (See Two ways to connect at the end of this part for the local/stdio alternative.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 2 — Open the Claude Desktop config
&lt;/h3&gt;

&lt;p&gt;In Claude Desktop: &lt;strong&gt;Settings → Developer → Edit Config&lt;/strong&gt;. That opens &lt;code&gt;claude_desktop_config.json&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;macOS:&lt;/strong&gt; &lt;code&gt;~/Library/Application Support/Claude/claude_desktop_config.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows:&lt;/strong&gt; &lt;code&gt;%APPDATA%\Claude\claude_desktop_config.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3 — Add the server
&lt;/h3&gt;

&lt;p&gt;Add an entry under &lt;code&gt;mcpServers&lt;/code&gt;. The key (&lt;code&gt;laravel-assistant&lt;/code&gt;) is the display name; the command runs &lt;code&gt;mcp-remote&lt;/code&gt; pointed at your endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"laravel-assistant"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"mcp-remote"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:8000/mcp/assistant"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;If you already have other servers in &lt;code&gt;mcpServers&lt;/code&gt;, add &lt;code&gt;laravel-assistant&lt;/code&gt; alongside them — don't replace the whole object.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 4 — Restart Claude Desktop
&lt;/h3&gt;

&lt;p&gt;Fully quit and reopen Claude Desktop (a window reload isn't enough — it spawns the server processes on launch). When it comes back, your tools appear in the chat's tool menu.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5 — Try it
&lt;/h3&gt;

&lt;p&gt;Ask Claude a question your server can answer from its data:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Using the Assistant tools, what were our top 5 customers by revenue, and which sales rep owns each?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude reads the schema resource, picks the right tool (or composes a read-only &lt;code&gt;SELECT&lt;/code&gt;), and answers from your live data — exactly the workflow the &lt;code&gt;#[Instructions]&lt;/code&gt; block describes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Securing it for real use
&lt;/h3&gt;

&lt;p&gt;The setup above is unauthenticated — fine for &lt;code&gt;localhost&lt;/code&gt;, dangerous anywhere else. &lt;strong&gt;Never expose an unauthenticated MCP server publicly&lt;/strong&gt;; it's a direct line to your data. Two things make it production-safe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Authenticate who can ask.&lt;/strong&gt; Protect the route and pass a token from the client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// routes/ai.php — protect with Sanctum&lt;/span&gt;
&lt;span class="nc"&gt;Mcp&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;web&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/mcp/assistant'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;AssistantServer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'auth:sanctum'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'throttle:mcp'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// claude_desktop_config.json — pass the bearer token through the bridge&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"laravel-assistant"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-remote"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"https://your-app.com/mcp/assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--header"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Authorization: Bearer YOUR_SANCTUM_TOKEN"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the broadest client compatibility, &lt;code&gt;laravel/mcp&lt;/code&gt; also supports &lt;strong&gt;OAuth 2.1 via Laravel Passport&lt;/strong&gt; (&lt;code&gt;Mcp::oauthRoutes()&lt;/code&gt; + &lt;code&gt;auth:api&lt;/code&gt; middleware), the mechanism the MCP spec recommends. Reach for it when you need to support clients that only speak OAuth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Scope what the app can read.&lt;/strong&gt; Authentication decides &lt;em&gt;who&lt;/em&gt; can ask; the database grant decides &lt;em&gt;what the app can read on their behalf&lt;/em&gt;. As Part 1 covered, every returned row leaves your perimeter — so the read-only user behind your assistant connection should hold &lt;code&gt;SELECT&lt;/code&gt; on &lt;strong&gt;only the tables you mean to expose&lt;/strong&gt;. The application guardrails are your first line; a scoped grant is the backstop if one is ever bypassed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'assistant_ro'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'a-strong-password'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;app_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt;     &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'assistant_ro'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;app_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;        &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'assistant_ro'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;app_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_details&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'assistant_ro'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;app_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;      &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'assistant_ro'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;app_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payments&lt;/span&gt;      &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'assistant_ro'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- ...one line per table you expose; nothing else.&lt;/span&gt;

&lt;span class="n"&gt;FLUSH&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point the assistant connection at that user, then &lt;strong&gt;verify the grant from the database's side&lt;/strong&gt; — don't trust the config comment, check it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Every line should be SELECT on a table you intend to expose —&lt;/span&gt;
&lt;span class="c"&gt;# no other tables, no INSERT/UPDATE/DELETE, no schema-wide "app_db.*".&lt;/span&gt;
mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SHOW GRANTS FOR 'assistant_ro'@'%';"&lt;/span&gt;

&lt;span class="c"&gt;# And confirm the negative cases are denied:&lt;/span&gt;
mysql &lt;span class="nt"&gt;-u&lt;/span&gt; assistant_ro &lt;span class="nt"&gt;-p&lt;/span&gt; app_db &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"DELETE FROM orders LIMIT 1;"&lt;/span&gt;   &lt;span class="c"&gt;# must fail&lt;/span&gt;
mysql &lt;span class="nt"&gt;-u&lt;/span&gt; assistant_ro &lt;span class="nt"&gt;-p&lt;/span&gt; app_db &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM users LIMIT 1;"&lt;/span&gt;  &lt;span class="c"&gt;# must fail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a write or a non-exposed table succeeds, the grant is too broad — fix it before going live. A scoped grant means even a bug in your query guard can only ever read the tables you already chose to expose. (For zero impact on production load, point it at a read replica.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Two ways to connect
&lt;/h3&gt;

&lt;p&gt;We used a &lt;strong&gt;web&lt;/strong&gt; server plus the &lt;code&gt;mcp-remote&lt;/code&gt; bridge because it's the same endpoint you'd authenticate and deploy for real. But Laravel MCP supports a second transport that's often simpler for purely local use:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Web&lt;/strong&gt; (&lt;code&gt;Mcp::web&lt;/code&gt;, used above)&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Local / stdio&lt;/strong&gt; (&lt;code&gt;Mcp::local&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who starts the server&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;You do&lt;/strong&gt; — &lt;code&gt;php artisan serve&lt;/code&gt;, kept running&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Claude Desktop does&lt;/strong&gt; — it runs the command for you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client config&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npx mcp-remote &amp;lt;url&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;php artisan mcp:start &amp;lt;handle&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Sanctum / OAuth over HTTP&lt;/td&gt;
&lt;td&gt;Inherits your CLI environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Shared/remote access, production&lt;/td&gt;
&lt;td&gt;One developer, one machine&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To use the local transport, register the server with &lt;code&gt;Mcp::local('assistant', AssistantServer::class)&lt;/code&gt; in &lt;code&gt;routes/ai.php&lt;/code&gt;, then have Claude Desktop launch it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"laravel-assistant"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/opt/homebrew/bin/php"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"/absolute/path/to/your-app/artisan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"mcp:start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Use absolute paths.&lt;/strong&gt; Claude Desktop doesn't inherit your shell's &lt;code&gt;PATH&lt;/code&gt;, so &lt;code&gt;"command": "php"&lt;/code&gt; will usually fail to launch. Point at the full binary (&lt;code&gt;which php&lt;/code&gt; → e.g. &lt;code&gt;/opt/homebrew/bin/php&lt;/code&gt;) and the full path to your project's &lt;code&gt;artisan&lt;/code&gt;. With this transport there's nothing to start by hand — Claude Desktop spawns the process — but you trade away the HTTP auth story from the previous section.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You've gone the full loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;understood MCP&lt;/strong&gt; as a self-describing interface built for models, not developers;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tested a server&lt;/strong&gt; in isolation with the Inspector;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;made the tools agent-native&lt;/strong&gt; with annotations, structured output, and truncation signalling;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;connected it to Claude Desktop&lt;/strong&gt; and locked down both &lt;em&gt;who can ask&lt;/em&gt; and &lt;em&gt;what the app can read&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same patterns scale to any Laravel application: expose your domain as a handful of well-described tools, return structured data, and be deliberate about what leaves your perimeter.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Get the code.&lt;/strong&gt; The complete, runnable server used throughout this tutorial — every tool, the schema resource, the prompt, and the test suite — is in the accompanying project: &lt;a href="//github.com/tomshaw/laravel-mcp-demo"&gt;laravel-mcp-demo&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>laravel</category>
      <category>mcp</category>
      <category>ai</category>
      <category>php</category>
    </item>
    <item>
      <title>Building a Streaming LLM Agent with the Laravel AI SDK</title>
      <dc:creator>Tom Shaw</dc:creator>
      <pubDate>Sun, 07 Jun 2026 05:09:27 +0000</pubDate>
      <link>https://dev.to/tomshaw/building-a-react-chat-agent-with-the-laravel-ai-sdk-4ip6</link>
      <guid>https://dev.to/tomshaw/building-a-react-chat-agent-with-the-laravel-ai-sdk-4ip6</guid>
      <description>&lt;h2&gt;
  
  
  Building a Streaming LLM Agent with the Laravel AI SDK
&lt;/h2&gt;

&lt;p&gt;A raw language model is a closed book. It can write, reason, and explain, but it&lt;br&gt;
only knows what it absorbed during training, and it can only ever produce text.&lt;br&gt;
Ask it the current time, today's exchange rate, an exact arithmetic result, or&lt;br&gt;
anything that changed last week, and it has two choices: admit it doesn't know,&lt;br&gt;
or guess convincingly. Neither is what you want in a product.&lt;/p&gt;

&lt;p&gt;The fix is to stop treating the model as an oracle and start treating it as the&lt;br&gt;
reasoning core of a larger system — one that can &lt;em&gt;take actions&lt;/em&gt;, see what comes&lt;br&gt;
back, and reason again. That system is what we call an &lt;strong&gt;agent&lt;/strong&gt;, and this guide&lt;br&gt;
walks through building a real one: a streaming chat assistant that calls tools,&lt;br&gt;
remembers conversations, and renders its answer token by token in the browser.&lt;/p&gt;


&lt;h2&gt;
  
  
  Agents, and the Laravel AI SDK
&lt;/h2&gt;

&lt;p&gt;The intro called that system an &lt;em&gt;agent&lt;/em&gt;. Here is the idea precisely. An LLM agent&lt;br&gt;
is a language model wrapped in a loop that lets it &lt;em&gt;act&lt;/em&gt; on the world instead of&lt;br&gt;
answering in a single shot. The model is no longer just predicting the next token&lt;br&gt;
of a reply — at each step it decides whether it has enough information to answer,&lt;br&gt;
or whether it needs to reach for a &lt;strong&gt;tool&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The dominant pattern for this is &lt;strong&gt;ReAct&lt;/strong&gt; — short for &lt;strong&gt;Rea&lt;/strong&gt;soning +&lt;br&gt;
&lt;strong&gt;Act&lt;/strong&gt;ing. Rather than answering all at once, the model works through a cycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reason&lt;/strong&gt; — think about what the question actually requires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Act&lt;/strong&gt; — if it needs outside information, call a &lt;em&gt;tool&lt;/em&gt; (a function you
provide) instead of guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observe&lt;/strong&gt; — the tool runs and returns a result, which is fed back to the
model as a new observation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeat&lt;/strong&gt; — the model reasons about that observation and either calls another
tool or produces its final answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A worked example — &lt;em&gt;"What time is it in Tokyo right now?"&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User:    What time is it in Tokyo right now?

Reason:  I don't actually know the current time. I should call a clock tool.
Act:     current_datetime(timezone: "Asia/Tokyo")
Observe: Saturday, June 6, 2026 at 11:42 PM (Asia/Tokyo)
Reason:  I now have the real local time. I can answer.
Answer:  It's currently 11:42 PM on Saturday, June 6, 2026 in Tokyo.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key move is that the model never &lt;em&gt;fakes&lt;/em&gt; the time. It recognises the gap in&lt;br&gt;
its knowledge, reaches for a tool, and reasons over the real result. That loop —&lt;br&gt;
reason, act, observe, repeat — is the entire idea behind an agent.&lt;/p&gt;

&lt;p&gt;Building one by hand, though, means solving the same set of problems every time:&lt;br&gt;
speaking each provider's particular HTTP dialect, describing tools in a format the&lt;br&gt;
model understands, parsing tool calls back out of the response, driving the&lt;br&gt;
reason/act/observe loop, and threading conversation history through all of it. The&lt;br&gt;
&lt;strong&gt;Laravel AI SDK&lt;/strong&gt; (&lt;code&gt;laravel/ai&lt;/code&gt;) is the first-party package that solves those&lt;br&gt;
problems once, behind an API that feels like the rest of Laravel.&lt;/p&gt;

&lt;p&gt;The shift it asks you to make is this: &lt;strong&gt;you stop writing procedural code that&lt;br&gt;
calls an API, and you start describing an agent as a class.&lt;/strong&gt; A system prompt, a&lt;br&gt;
model, and a list of tools become declarations on a PHP class; the SDK reads that&lt;br&gt;
class and does the orchestration. It speaks to every major provider — Anthropic,&lt;br&gt;
OpenAI, Gemini, Groq, Ollama — through the same interface, so switching models is&lt;br&gt;
a one-line change rather than a rewrite.&lt;/p&gt;

&lt;p&gt;Until recently, that kind of tooling lived almost entirely in Python — LangChain,&lt;br&gt;
LlamaIndex, the provider-native agent frameworks. For a Laravel team, adopting&lt;br&gt;
them means standing up a &lt;em&gt;second&lt;/em&gt; service in a &lt;em&gt;second&lt;/em&gt; language and talking to it&lt;br&gt;
over HTTP, which quietly puts your AI logic on the wrong side of a boundary: away&lt;br&gt;
from your Eloquent models, your auth gates, your queue, and your test suite.&lt;/p&gt;

&lt;p&gt;This is where the Laravel AI SDK is genuinely hard to beat — and it isn't about&lt;br&gt;
cleverer abstractions. It's that the agent lives &lt;em&gt;inside&lt;/em&gt; your application. A tool&lt;br&gt;
is just PHP, so it can query Eloquent, respect a Gate, or dispatch a job directly.&lt;br&gt;
Persistence is migrations and Eloquent models. Streaming rides Livewire. Tests use&lt;br&gt;
the same Pest fakes as everything else. There's no glue service and no&lt;br&gt;
serialization boundary to cross — the model's reasoning and your domain logic run&lt;br&gt;
in the same process, with the same tools you already know.&lt;/p&gt;

&lt;p&gt;Each thing an agent needs maps onto a concept the SDK gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;agent&lt;/strong&gt; itself is a class you prompt. It carries the instructions, the
model choice, and the tools, and exposes methods like &lt;code&gt;prompt()&lt;/code&gt; and &lt;code&gt;stream()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;tool&lt;/strong&gt; is a capability you grant that agent — a class with a name, a
description, a parameter schema, and a &lt;code&gt;handle()&lt;/code&gt; method that does the work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming&lt;/strong&gt; lets you consume the reply as it forms, as a sequence of typed
events (text fragments, tool calls) rather than one final blob.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversation memory&lt;/strong&gt; persists each turn and replays prior history, so a
multi-turn chat just works without you managing state by hand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The single most important thing the SDK does, though, is &lt;strong&gt;run the agent loop for&lt;br&gt;
you.&lt;/strong&gt; You hand it tools and a step limit; it asks the model what it wants to do,&lt;br&gt;
executes the chosen tool, feeds the result back, and repeats until the model has&lt;br&gt;
a final answer. The whole ReAct cycle happens inside the SDK — you never write the&lt;br&gt;
loop yourself.&lt;/p&gt;
&lt;h3&gt;
  
  
  It's built on Prism
&lt;/h3&gt;

&lt;p&gt;The SDK doesn't reinvent provider communication from scratch. Underneath, it's&lt;br&gt;
built on &lt;strong&gt;&lt;a href="https://prismphp.com/" rel="noopener noreferrer"&gt;Prism&lt;/a&gt;&lt;/strong&gt;, and the two relate the way&lt;br&gt;
&lt;strong&gt;Eloquent relates to the Query Builder&lt;/strong&gt;: Prism is the lower-level engine that&lt;br&gt;
normalises providers and raw LLM calls, and the Laravel AI SDK is the&lt;br&gt;
higher-level, opinionated framework on top — the layer that adds agents, tools,&lt;br&gt;
memory, structured output, streaming, and testing helpers.&lt;/p&gt;

&lt;p&gt;That layering is concrete, not just a metaphor: &lt;code&gt;laravel/ai&lt;/code&gt; declares&lt;br&gt;
&lt;code&gt;prism-php/prism&lt;/code&gt; as a Composer dependency, so a call to &lt;code&gt;$agent-&amp;gt;stream()&lt;/code&gt;&lt;br&gt;
ultimately drives Prism, which drives the provider's HTTP API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your app  →  Laravel AI SDK  →  Prism  →  Anthropic / OpenAI / …
            (agents, tools,     (provider
             memory, streaming)  normalisation)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The practical guidance follows directly from that: &lt;strong&gt;build on the Laravel AI SDK.&lt;/strong&gt;&lt;br&gt;
It's the layer meant for application code, and it's where everything in this guide&lt;br&gt;
lives. Knowing Prism is underneath is useful — if you ever need something the SDK&lt;br&gt;
hasn't surfaced yet, you can drop down to it directly, exactly as you'd&lt;br&gt;
occasionally reach past Eloquent for the Query Builder.&lt;/p&gt;


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

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; PHP 8.5 · Laravel 13 · Livewire 4 · Laravel AI SDK&lt;/p&gt;

&lt;p&gt;Enough theory — let's wire it up, starting with the one piece of setup the SDK&lt;br&gt;
can't infer: where to send requests, and with what key. Both live in your&lt;br&gt;
environment. We'll use Anthropic throughout, but any supported provider works the&lt;br&gt;
same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .env
AI_PROVIDER=anthropic
ANTHROPIC_API_KEY=sk-ant-...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;config/ai.php&lt;/code&gt; reads those values. The default provider falls back to&lt;br&gt;
&lt;code&gt;anthropic&lt;/code&gt;, and the Anthropic driver is wired to the API key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/ai.php&lt;/span&gt;
&lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AI_PROVIDER'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'anthropic'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="s1"&gt;'providers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="s1"&gt;'anthropic'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'driver'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'anthropic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'key'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ANTHROPIC_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ANTHROPIC_URL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'https://api.anthropic.com/v1'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You rarely reference the string &lt;code&gt;'anthropic'&lt;/code&gt; in code, though. The SDK ships a&lt;br&gt;
type-safe &lt;code&gt;Lab&lt;/code&gt; enum (&lt;code&gt;Laravel\Ai\Enums\Lab&lt;/code&gt;) that you attach to an agent — which&lt;br&gt;
is exactly what we'll do next.&lt;/p&gt;


&lt;h2&gt;
  
  
  Building the agent
&lt;/h2&gt;

&lt;p&gt;An agent is one small class. Read it once, then we'll break it down.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ChatAgent.php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Ai\Tools\Calculator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Ai\Tools\CurrentDateTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Ai\Tools\WikipediaLookup&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Attributes\MaxSteps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Attributes\Model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Attributes\Provider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Attributes\Temperature&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Concerns\RemembersConversations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Contracts\Agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Contracts\Conversational&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Contracts\HasTools&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Enums\Lab&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Promptable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Providers\Tools\WebSearch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Stringable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Provider(Lab::Anthropic)]&lt;/span&gt;
&lt;span class="na"&gt;#[Model('claude-sonnet-4-6')]&lt;/span&gt;
&lt;span class="na"&gt;#[Temperature(0.7)]&lt;/span&gt;
&lt;span class="na"&gt;#[MaxSteps(8)]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatAgent&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Conversational&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HasTools&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Promptable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RemembersConversations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Stringable&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;&amp;lt;&amp;lt;&amp;lt;'PROMPT'
        You are a friendly, knowledgeable assistant that answers using the ReAct pattern:
        Reason about the question, Act by calling a tool when it helps, observe the
        result, then continue until you can give a clear final answer.

        Guidelines:
        - Think step by step, but keep your final answer concise and well formatted (Markdown).
        - Use the `calculator` tool for any arithmetic instead of computing in your head.
        - Use the `current_datetime` tool whenever the user asks about the current date or time.
        - Use the `wikipedia_lookup` tool for factual background on a specific topic, person, or place.
        - Use web search for recent events or anything that may have changed after your training.
        - If a tool fails or returns nothing useful, say so honestly rather than guessing.
        PROMPT;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;iterable&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Calculator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CurrentDateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WikipediaLookup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSearch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The attributes
&lt;/h3&gt;

&lt;p&gt;PHP attributes configure the agent declaratively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;#[Provider(Lab::Anthropic)]&lt;/code&gt; — which provider to talk to, using the type-safe
&lt;code&gt;Lab&lt;/code&gt; enum rather than a magic string.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#[Model('claude-sonnet-4-6')]&lt;/code&gt; — the specific model.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#[Temperature(0.7)]&lt;/code&gt; — sampling randomness; 0.7 is a balanced default for a
conversational assistant.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#[MaxSteps(8)]&lt;/code&gt; — &lt;strong&gt;the ReAct loop bound.&lt;/strong&gt; Each "step" is one
reason→act→observe cycle. With &lt;code&gt;8&lt;/code&gt;, the agent may call tools and reason up to
eight times before it must produce a final answer. This is your safety valve
against a model that loops forever calling tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The contracts and traits
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatAgent&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Conversational&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HasTools&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Promptable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RemembersConversations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;implements Agent&lt;/code&gt; — marks the class as an agent.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;implements HasTools&lt;/code&gt; — declares that this agent exposes tools (via &lt;code&gt;tools()&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;implements Conversational&lt;/code&gt; — declares that this agent participates in
persisted, multi-turn conversations.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;use Promptable&lt;/code&gt; — adds the &lt;code&gt;prompt()&lt;/code&gt; and &lt;code&gt;stream()&lt;/code&gt; methods you call to run
the agent.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;use RemembersConversations&lt;/code&gt; — automatically persists each user/assistant turn
and replays prior history so the model has context. Conversation memory, for
free.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The system prompt
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;instructions()&lt;/code&gt; returns the system prompt, and it's doing two jobs at once: it&lt;br&gt;
tells the model to follow the ReAct pattern, and it gives concrete guidance on&lt;br&gt;
&lt;em&gt;which&lt;/em&gt; tool to prefer for &lt;em&gt;which&lt;/em&gt; kind of question. Good tool descriptions plus&lt;br&gt;
clear prompt guidance are what make the model reach for the right tool at the&lt;br&gt;
right time.&lt;/p&gt;
&lt;h3&gt;
  
  
  The tools
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;tools()&lt;/code&gt; returns the list the model is allowed to call. Three are custom classes&lt;br&gt;
(&lt;code&gt;Calculator&lt;/code&gt;, &lt;code&gt;CurrentDateTime&lt;/code&gt;, &lt;code&gt;WikipediaLookup&lt;/code&gt;); the fourth, &lt;code&gt;WebSearch&lt;/code&gt;, is&lt;br&gt;
a built-in &lt;strong&gt;provider tool&lt;/strong&gt; shipped by the SDK — capped here to five results&lt;br&gt;
with &lt;code&gt;-&amp;gt;max(5)&lt;/code&gt;. Let's write one.&lt;/p&gt;


&lt;h2&gt;
  
  
  Writing tools
&lt;/h2&gt;

&lt;p&gt;A tool is any class implementing &lt;code&gt;Laravel\Ai\Contracts\Tool&lt;/code&gt;. The contract is&lt;br&gt;
four methods: &lt;code&gt;name()&lt;/code&gt;, &lt;code&gt;description()&lt;/code&gt;, &lt;code&gt;schema()&lt;/code&gt;, and &lt;code&gt;handle()&lt;/code&gt;. The model&lt;br&gt;
reads the name, description, and schema to decide &lt;em&gt;whether&lt;/em&gt; and &lt;em&gt;how&lt;/em&gt; to call the&lt;br&gt;
tool; &lt;code&gt;handle()&lt;/code&gt; does the actual work and returns an observation string.&lt;/p&gt;
&lt;h3&gt;
  
  
  A minimal tool: the current date and time
&lt;/h3&gt;

&lt;p&gt;The clock is the cleanest example of an "observation" tool — the model simply&lt;br&gt;
cannot know the real current time, so it has to ask.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CurrentDateTime.php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Carbon\CarbonImmutable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Contracts\JsonSchema\JsonSchema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Contracts\Tool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Tools\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Stringable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CurrentDateTime&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Tool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'current_datetime'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Stringable&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'Get the current date and time. Optionally pass an IANA timezone '&lt;/span&gt;
            &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'(e.g. "Asia/Tokyo", "America/New_York") to get the local time there.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;JsonSchema&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'timezone'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'An IANA timezone identifier such as "Europe/Paris". Defaults to UTC.'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Stringable&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$timezone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'timezone'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'UTC'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$timezone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timezone_identifiers_list&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Unknown timezone &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$timezone&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;. Please use an IANA identifier like &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Asia/Tokyo&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CarbonImmutable&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$timezone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$now&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'l, F j, Y \a\t g:i A'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;' ('&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$timezone&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;')'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth noticing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;name()&lt;/code&gt;&lt;/strong&gt; is the identifier the model uses when it decides to call the tool.
Keep it short and snake_case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;description()&lt;/code&gt;&lt;/strong&gt; is sales copy aimed at the model. The clearer you describe
&lt;em&gt;when&lt;/em&gt; to use the tool and &lt;em&gt;what each parameter means&lt;/em&gt;, the more reliably the
model calls it correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;schema()&lt;/code&gt;&lt;/strong&gt; declares the parameters using a fluent JSON-schema builder.
Here, &lt;code&gt;timezone&lt;/code&gt; is an optional string (no &lt;code&gt;-&amp;gt;required()&lt;/code&gt;), with its own
description so the model knows to pass an IANA identifier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;handle(Request $request)&lt;/code&gt;&lt;/strong&gt; receives the model's arguments as a &lt;code&gt;Request&lt;/code&gt;
object. &lt;code&gt;$request-&amp;gt;string('timezone', 'UTC')&lt;/code&gt; reads the argument with a
default. The returned string is what the model "observes." Note that we
&lt;strong&gt;validate&lt;/strong&gt; the timezone and return a helpful message instead of throwing — the
model can read that message and recover.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A tool that calls an external API
&lt;/h3&gt;

&lt;p&gt;Tools can do real I/O. This one reaches out to Wikipedia's REST API and returns a&lt;br&gt;
summary — and, importantly, it handles every failure path gracefully so the model&lt;br&gt;
always gets a usable observation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WikipediaLookup.php&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;JsonSchema&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'topic'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'The Wikipedia article title to summarize, e.g. "Great Barrier Reef".'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Stringable&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$topic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'topic'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$topic&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'No topic was provided to look up.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;acceptJson&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withHeaders&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'User-Agent'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'LaravelAiSdkDemo/1.0 (tutorial)'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://en.wikipedia.org/api/rest_v1/page/summary/'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;rawurlencode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$topic&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Wikipedia lookup for &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$topic&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"No Wikipedia article was found for &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$topic&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Wikipedia lookup for &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$topic&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; failed with status &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$extract&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;jsonString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'extract'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$extract&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"No summary is available for &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$topic&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;jsonString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$topic&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;jsonString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'content_urls.desktop.page'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"**&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;**&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$extract&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Source: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern here &lt;em&gt;is&lt;/em&gt; the lesson: &lt;strong&gt;a tool should never throw raw exceptions at&lt;br&gt;
the model.&lt;/strong&gt; A timeout, a 404, an empty body — each becomes a plain-English string&lt;br&gt;
the model can reason about ("the lookup failed, I'll tell the user honestly"),&lt;br&gt;
exactly as the system prompt instructs. Note that &lt;code&gt;topic&lt;/code&gt; is &lt;code&gt;-&amp;gt;required()&lt;/code&gt;, and&lt;br&gt;
the tool returns lightly-formatted Markdown including a source link.&lt;/p&gt;
&lt;h3&gt;
  
  
  A tool that must be safe: the calculator
&lt;/h3&gt;

&lt;p&gt;A calculator lets the model do exact arithmetic. The interesting part is what it&lt;br&gt;
&lt;em&gt;doesn't&lt;/em&gt; do — it never calls &lt;code&gt;eval()&lt;/code&gt;. Instead it tokenizes the expression and&lt;br&gt;
walks a tiny recursive-descent grammar that only understands numbers, the&lt;br&gt;
operators &lt;code&gt;+ - * / % ^&lt;/code&gt;, parentheses, and unary minus. Anything else is rejected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Calculator.php&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;JsonSchema&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'expression'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'The arithmetic expression to evaluate, e.g. "3 * (4 + 5)".'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Stringable&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$expression&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'expression'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$expression&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Could not evaluate &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$expression&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Render integers without a trailing ".0" for nicer output.&lt;/span&gt;
    &lt;span class="nv"&gt;$formatted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;
        &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;
        &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$expression&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$formatted&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The takeaway: model-supplied input is &lt;strong&gt;untrusted&lt;/strong&gt;. A naive &lt;code&gt;eval($expression)&lt;/code&gt;&lt;br&gt;
would be a remote code execution hole, because the model (or a user steering it)&lt;br&gt;
controls that string. A private &lt;code&gt;evaluate()&lt;/code&gt; method that parses safely is the&lt;br&gt;
right call. The same principle applies any time a tool touches your filesystem,&lt;br&gt;
your shell, or your database — treat the arguments as adversarial.&lt;/p&gt;
&lt;h3&gt;
  
  
  Built-in provider tools
&lt;/h3&gt;

&lt;p&gt;Not every tool is yours to write. &lt;code&gt;(new WebSearch)-&amp;gt;max(5)&lt;/code&gt; from earlier is a&lt;br&gt;
provider tool (&lt;code&gt;Laravel\Ai\Providers\Tools\WebSearch&lt;/code&gt;) — the provider runs the&lt;br&gt;
search natively. You compose it alongside your custom tools in the same list, and&lt;br&gt;
the model treats them all the same way.&lt;/p&gt;


&lt;h2&gt;
  
  
  Streaming the answer to the browser
&lt;/h2&gt;

&lt;p&gt;Tools and the agent loop are the brain; streaming is how the user actually&lt;br&gt;
experiences it. Rather than blocking until the full answer is ready, you iterate&lt;br&gt;
over the response and push fragments to the browser as they arrive.&lt;/p&gt;

&lt;p&gt;Here's the heart of it — a component method that takes a prompt, runs the agent,&lt;br&gt;
and streams the reply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Ai\Agents\ChatAgent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Streaming\Events\TextDelta&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Streaming\Events\ToolCall&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$prompt&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// The ReAct loop streams over a long-lived request (tool calls, web&lt;/span&gt;
    &lt;span class="c1"&gt;// search, multi-step reasoning); lift the request time limit so the&lt;/span&gt;
    &lt;span class="c1"&gt;// response isn't truncated mid-stream by PHP's max_execution_time.&lt;/span&gt;
    &lt;span class="nb"&gt;set_time_limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;conversationId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
        &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;ChatAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;forUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ChatAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$agent&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;ToolCall&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Using '&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;toolCall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'…'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;elseif&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;TextDelta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Render the accumulated answer as Markdown on each delta so the&lt;/span&gt;
            &lt;span class="c1"&gt;// live stream shows formatted text (not raw Markdown), matching&lt;/span&gt;
            &lt;span class="c1"&gt;// how the persisted message is rendered once the turn completes.&lt;/span&gt;
            &lt;span class="nv"&gt;$answer&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'answer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'html_input'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'escape'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'allow_unsafe_links'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                &lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// After the stream finishes, the SDK has persisted the conversation.&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;conversationId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'conversation-updated'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step by step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;set_time_limit(0)&lt;/code&gt;&lt;/strong&gt; — a ReAct turn can involve several tool calls and a web&lt;br&gt;
search, so it may outlast PHP's default &lt;code&gt;max_execution_time&lt;/code&gt;. Lifting the&lt;br&gt;
limit prevents the stream from being cut off mid-answer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Starting vs continuing a conversation&lt;/strong&gt; — &lt;code&gt;ChatAgent::make()&lt;/code&gt; builds the&lt;br&gt;
agent. &lt;code&gt;-&amp;gt;forUser($user)&lt;/code&gt; starts a &lt;strong&gt;new&lt;/strong&gt; conversation owned by that user;&lt;br&gt;
&lt;code&gt;-&amp;gt;continue($conversationId, as: $user)&lt;/code&gt; resumes an existing one so the model&lt;br&gt;
sees prior history. The component just tracks a &lt;code&gt;conversationId&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;$agent-&amp;gt;stream($prompt)&lt;/code&gt;&lt;/strong&gt; — runs the agent and returns an iterable stream&lt;br&gt;
instead of a single blob. The &lt;code&gt;Promptable&lt;/code&gt; trait provides this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The event loop&lt;/strong&gt; — iterating the response yields typed events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ToolCall&lt;/code&gt; — the model decided to &lt;em&gt;act&lt;/em&gt;. Surface a small "Using calculator…"
status so the user can see the agent reaching for a tool;
&lt;code&gt;$event-&amp;gt;toolCall-&amp;gt;name&lt;/code&gt; is the tool's &lt;code&gt;name()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TextDelta&lt;/code&gt; — a fragment of the final answer. Accumulate fragments into
&lt;code&gt;$answer&lt;/code&gt;, render the running total as Markdown, and push it to the &lt;code&gt;answer&lt;/code&gt;
target with &lt;code&gt;replace: true&lt;/code&gt; (replace, not append, because we re-render the
whole accumulated Markdown each time).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;After the loop&lt;/strong&gt; — because of &lt;code&gt;RemembersConversations&lt;/code&gt;, the SDK has already&lt;br&gt;
persisted both the user prompt and the assistant reply. Read the (possibly&lt;br&gt;
brand-new) &lt;code&gt;conversationId&lt;/code&gt; back off the response and refresh the UI.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Where the stream lands in the markup
&lt;/h3&gt;

&lt;p&gt;Streaming to a named target (&lt;code&gt;to: 'answer'&lt;/code&gt;, &lt;code&gt;to: 'status'&lt;/code&gt;) maps onto regions in&lt;br&gt;
the view via &lt;code&gt;wire:stream&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div x-show="streaming" x-cloak class="flex flex-col gap-2"&amp;gt;
    &amp;lt;div x-ref="status" wire:stream="status" class="text-xs font-medium text-accent"&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div class="max-w-[90%] text-[15px]"&amp;gt;
        &amp;lt;span x-show="!hasAnswer" class="inline-flex gap-1 align-middle"&amp;gt;
            {{-- animated "typing" dots while we wait for the first token --}}
        &amp;lt;/span&amp;gt;
        &amp;lt;div x-ref="answer" wire:stream="answer" class="reply" :class="hasAnswer &amp;amp;&amp;amp; 'stream-caret'"&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;wire:stream="answer"&lt;/code&gt; element receives each streamed update directly in the&lt;br&gt;
browser — no full round-trip per token. A small amount of client-side JavaScript&lt;br&gt;
handles the niceties: showing the user's question optimistically the instant they&lt;br&gt;
hit send, disabling the composer while streaming, auto-scrolling as the reply&lt;br&gt;
grows, and resetting on a conversation switch.&lt;/p&gt;

&lt;p&gt;The result: the user sees a "Using calculator…" pill, then the answer typing&lt;br&gt;
itself out live, then a clean persisted message — all from a single method.&lt;/p&gt;


&lt;h2&gt;
  
  
  Persisting conversations
&lt;/h2&gt;

&lt;p&gt;Notice there was no persistence code in &lt;code&gt;send()&lt;/code&gt; — the SDK handled it. Three&lt;br&gt;
pieces make that work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The user owns conversations.&lt;/strong&gt; The &lt;code&gt;User&lt;/code&gt; model uses the SDK's&lt;br&gt;
&lt;code&gt;HasConversations&lt;/code&gt; trait, which is what makes &lt;code&gt;-&amp;gt;forUser($user)&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;-&amp;gt;continue(..., as: $user)&lt;/code&gt; work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Concerns\HasConversations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Authenticatable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HasConversations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HasFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Notifiable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. The agent remembers.&lt;/strong&gt; The &lt;code&gt;RemembersConversations&lt;/code&gt; trait on the agent is&lt;br&gt;
what actually writes each turn and replays history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The schema.&lt;/strong&gt; A migration extending &lt;code&gt;Laravel\Ai\Migrations\AiMigration&lt;/code&gt;&lt;br&gt;
creates two tables — one for conversations, one for messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AiMigration&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$conversationsTable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ai.conversations.tables.conversations'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'agent_conversations'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$messagesTable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ai.conversations.tables.messages'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'agent_conversation_messages'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$conversationsTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'updated_at'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$messagesTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'conversation_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'agent'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'role'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'content'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'attachments'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tool_calls'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tool_results'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'usage'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'meta'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'conversation_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'updated_at'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;'conversation_index'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each message row stores the &lt;code&gt;role&lt;/code&gt; (&lt;code&gt;user&lt;/code&gt; / &lt;code&gt;assistant&lt;/code&gt; / &lt;code&gt;tool&lt;/code&gt;), the&lt;br&gt;
&lt;code&gt;content&lt;/code&gt;, and — useful for a UI — the &lt;code&gt;tool_calls&lt;/code&gt; the assistant made. That's how&lt;br&gt;
you can render a "calculator" pill next to a persisted answer: read the saved&lt;br&gt;
&lt;code&gt;tool_calls&lt;/code&gt; for that message. You query these through the SDK's&lt;br&gt;
&lt;code&gt;Laravel\Ai\Models\Conversation&lt;/code&gt; and &lt;code&gt;Laravel\Ai\Models\ConversationMessage&lt;/code&gt;&lt;br&gt;
Eloquent models.&lt;/p&gt;


&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;A feature with this many moving parts — streaming, tool calls, persistence —&lt;br&gt;
needs tests, and the obvious approach is the wrong one: hitting a real model in&lt;br&gt;
tests would be slow, costly, and non-deterministic. Instead, the SDK provides a&lt;br&gt;
&lt;code&gt;fake()&lt;/code&gt; helper to swap the agent for a scripted response, plus assertions to&lt;br&gt;
verify it was prompted correctly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'streams an answer and persists the conversation'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;ChatAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'Hello from the agent!'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Livewire&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'chat.main'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'userId'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'send'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Hi there'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertSee&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Hi there'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertSee&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Hello from the agent!'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertDispatched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'conversation-updated'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$component&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'conversationId'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toBeNull&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;ChatAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;assertPrompted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Hi there'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ConversationMessage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'role'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ConversationMessage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'role'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'assistant'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this verifies, end to end:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ChatAgent::fake(['Hello from the agent!'])&lt;/code&gt; makes the agent return a canned
reply instead of calling the provider.&lt;/li&gt;
&lt;li&gt;The flow renders both the user's prompt and the agent's reply, and dispatches
the &lt;code&gt;conversation-updated&lt;/code&gt; event.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ChatAgent::assertPrompted('Hi there')&lt;/code&gt; confirms the agent received the exact
prompt.&lt;/li&gt;
&lt;li&gt;Both the user and assistant messages were persisted — proving the
&lt;code&gt;RemembersConversations&lt;/code&gt; wiring works.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a matching &lt;code&gt;assertNotPrompted()&lt;/code&gt; for the "ignore blank prompts" case, and&lt;br&gt;
tools are best covered by fast unit tests that exercise their &lt;code&gt;handle()&lt;/code&gt; logic and&lt;br&gt;
error paths directly — no model needed. Run the suite with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--compact&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Extending it: add your own tool
&lt;/h2&gt;

&lt;p&gt;Once the agent is in place, growing its capabilities is a tight, three-step loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create the tool.&lt;/strong&gt; &lt;code&gt;php artisan make:tool MyTool&lt;/code&gt; scaffolds a class.
Implement &lt;code&gt;name()&lt;/code&gt;, &lt;code&gt;description()&lt;/code&gt;, &lt;code&gt;schema()&lt;/code&gt;, and &lt;code&gt;handle()&lt;/code&gt; — model the
&lt;code&gt;description&lt;/code&gt;/&lt;code&gt;schema&lt;/code&gt; text carefully, since that's how the model decides to
call it. Return plain strings, including for errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Register it&lt;/strong&gt; in the agent's &lt;code&gt;tools()&lt;/code&gt; method by adding &lt;code&gt;new MyTool&lt;/code&gt; to the
array.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optionally mention it&lt;/strong&gt; in the agent's &lt;code&gt;instructions()&lt;/code&gt; so the model knows
when to prefer it, and &lt;strong&gt;test it&lt;/strong&gt; with a unit test plus a faked-agent feature
test.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the entire workflow. The SDK handles the reasoning, the tool calls, the&lt;br&gt;
streaming, and the persistence; you provide a well-described agent and a handful&lt;br&gt;
of safe, honest tools — and the ReAct pattern does the rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Source code
&lt;/h2&gt;

&lt;p&gt;Everything in this guide comes together in a complete, runnable application — a&lt;br&gt;
minimal ChatGPT-style assistant with a streaming UI, the four tools shown above,&lt;br&gt;
conversation history, and the full test suite. You can clone it, add your API&lt;br&gt;
key, and have a working agent in a few minutes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/tomshaw/laravel-aisdk-demo" rel="noopener noreferrer"&gt;Tom Shaw / laravel-aisdk-demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The agent&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app/Ai/Agents/ChatAgent.php&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app/Ai/Tools/*.php&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chat UI + streaming&lt;/td&gt;
&lt;td&gt;&lt;code&gt;resources/views/components/chat/⚡main.blade.php&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conversation owner&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app/Models/User.php&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistence schema&lt;/td&gt;
&lt;td&gt;&lt;code&gt;database/migrations/..._create_agent_conversations_table.php&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config&lt;/td&gt;
&lt;td&gt;&lt;code&gt;config/ai.php&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tests/Feature/ChatTest.php&lt;/code&gt;, &lt;code&gt;tests/Unit/*&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Clone it, read the agent class first, then follow a single prompt through&lt;br&gt;
&lt;code&gt;send()&lt;/code&gt; and watch the reason/act/observe loop play out token by token.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
