<?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: Maximilian Nussbaumer</title>
    <description>The latest articles on DEV Community by Maximilian Nussbaumer (@quotentiroler).</description>
    <link>https://dev.to/quotentiroler</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3743148%2F1ab199b2-2268-41f7-aa8a-3edf7de5e349.jpg</url>
      <title>DEV Community: Maximilian Nussbaumer</title>
      <link>https://dev.to/quotentiroler</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/quotentiroler"/>
    <language>en</language>
    <item>
      <title>Transport, Surface, Skin: Building MCP Plugins That Survive the Spec</title>
      <dc:creator>Maximilian Nussbaumer</dc:creator>
      <pubDate>Sat, 25 Jul 2026 12:57:57 +0000</pubDate>
      <link>https://dev.to/quotentiroler/transport-surface-skin-building-mcp-plugins-that-survive-the-spec-5dln</link>
      <guid>https://dev.to/quotentiroler/transport-surface-skin-building-mcp-plugins-that-survive-the-spec-5dln</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Updated 1 August 2026.&lt;/strong&gt; The 2026-07-28 revision has landed, the TypeScript SDK v2 is GA, and &lt;code&gt;@maxhealth.tech/mcp-http&lt;/code&gt; 0.3.0 has moved onto it. Corrections are called out where they apply.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Software engineering is dead. That is what they say.&lt;/p&gt;

&lt;p&gt;Partially true. If a model can write the function, fewer humans will sit down and type it out. But losing the typing is a long way from losing the engineering. What actually happened is that the interesting question moved up a level. It used to be &lt;em&gt;how do I build this application&lt;/em&gt;. Now it is &lt;em&gt;how do a human, an application, and a model share one workspace&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That question is why the Model Context Protocol went from a curiosity to infrastructure so fast. Everyone hears MCP as the model quietly calling your API in the background. What it actually means is the model, the user, and your app all operating on the same live surface, at the same time.&lt;/p&gt;

&lt;p&gt;So: how do you actually build one of these things?&lt;/p&gt;

&lt;h2&gt;
  
  
  The protocol is a moving target, and that is the point
&lt;/h2&gt;

&lt;p&gt;Anyone who shipped an MCP server in 2025 has rewritten it at least twice. The transport story alone went stdio, then SSE, then Streamable HTTP. Streamable HTTP won and is now the dominant way to build.&lt;/p&gt;

&lt;p&gt;And on &lt;strong&gt;28 July 2026&lt;/strong&gt; the next revision landed. It is the biggest one yet, because it makes MCP stateless.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;initialize&lt;/code&gt; / &lt;code&gt;initialized&lt;/code&gt; handshake is &lt;strong&gt;gone&lt;/strong&gt; (SEP-2575). Protocol version, client info and capabilities now ride in &lt;code&gt;_meta&lt;/code&gt; on every request, under keys like &lt;code&gt;io.modelcontextprotocol/protocolVersion&lt;/code&gt;. Protocol-level sessions are &lt;strong&gt;gone&lt;/strong&gt; too (SEP-2567), which means no more &lt;code&gt;Mcp-Session-Id&lt;/code&gt; and any request can hit any instance.&lt;/p&gt;

&lt;p&gt;The standalone GET stream endpoint is removed. Long lived streams did not disappear, they moved: change notifications now arrive on the response stream of a &lt;code&gt;subscriptions/listen&lt;/code&gt; POST, which stays open and carries only the notification types you opted into. What did die is resumability, because &lt;code&gt;Last-Event-ID&lt;/code&gt; and SSE event ids are gone outright. A broken stream now means re-issuing the request with a fresh id.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Servers no longer send requests to clients at all.&lt;/strong&gt; This is the deepest change. Sampling, elicitation and roots used to be the server asking the client a question mid-flight. They are now embedded in an &lt;code&gt;InputRequiredResult&lt;/code&gt;, which the client answers by &lt;em&gt;retrying the original call&lt;/em&gt; with matching &lt;code&gt;inputResponses&lt;/code&gt; (SEP-2322, "multi round trip requests"). The spec is blunt about it: a server "MUST NOT send independent JSON-RPC requests" on a response stream, and clients "MUST NOT send JSON-RPC responses" at all. Every exchange is now client-initiated, which is what makes the whole thing safe to load balance.&lt;/p&gt;

&lt;p&gt;A few smaller things travel with it. Two routing headers become required (SEP-2243): &lt;code&gt;Mcp-Method&lt;/code&gt; on every request, and &lt;code&gt;Mcp-Name&lt;/code&gt; on &lt;code&gt;tools/call&lt;/code&gt;, &lt;code&gt;resources/read&lt;/code&gt; and &lt;code&gt;prompts/get&lt;/code&gt; only, so intermediaries can route and rate limit on the operation without parsing a body. Mind the casing asymmetry when you write those down, because &lt;code&gt;MCP-Protocol-Version&lt;/code&gt; is all caps and has existed since 2025-06-18, while the new pair is &lt;code&gt;Mcp-&lt;/code&gt;. Cancellation on Streamable HTTP is now simply closing the response stream. Every result carries a required &lt;code&gt;resultType&lt;/code&gt; field. And list and read results gain required &lt;code&gt;ttlMs&lt;/code&gt; and &lt;code&gt;cacheScope&lt;/code&gt; fields via a new &lt;code&gt;CacheableResult&lt;/code&gt; interface (SEP-2549), where omitting them gets you the conservative &lt;code&gt;{ ttlMs: 0, cacheScope: 'private' }&lt;/code&gt; fallback and clients that re-fetch constantly.&lt;/p&gt;

&lt;p&gt;There is a second half to this revision that the first draft of this article skipped, and it lands squarely on the layer I claimed was solved. &lt;strong&gt;OAuth Dynamic Client Registration is deprecated&lt;/strong&gt; in favour of &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration#client-id-metadata-documents" rel="noopener noreferrer"&gt;Client ID Metadata Documents&lt;/a&gt;. RFC 7591 still works against authorization servers that have nothing else, but new implementations should not reach for it. Alongside that, authorization servers &lt;strong&gt;SHOULD&lt;/strong&gt; send the &lt;code&gt;iss&lt;/code&gt; parameter on authorization responses (&lt;a href="https://datatracker.ietf.org/doc/html/rfc9207" rel="noopener noreferrer"&gt;RFC 9207&lt;/a&gt;) and clients &lt;strong&gt;MUST&lt;/strong&gt; validate it before redeeming the code, clients must send an appropriate &lt;code&gt;application_type&lt;/code&gt; during DCR, and persisted credentials must be keyed by issuer so they are never replayed against a different authorization server.&lt;/p&gt;

&lt;p&gt;Finally, Roots, Sampling and Logging are deprecated on a twelve month clock under a new &lt;a href="https://modelcontextprotocol.io/community/feature-lifecycle" rel="noopener noreferrer"&gt;feature lifecycle policy&lt;/a&gt;, and &lt;code&gt;ping&lt;/code&gt;, &lt;code&gt;logging/setLevel&lt;/code&gt; and &lt;code&gt;notifications/roots/list_changed&lt;/code&gt; are removed outright.&lt;/p&gt;

&lt;p&gt;Read that list again and notice what it adds up to. Your MCP server stops being a stateful daemon you have to keep alive and turn into sticky sessions. It becomes a pure function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(Request, Token) -&amp;gt; Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is exactly the shape of a Cloudflare Worker, a Deno Deploy handler, a Pages Function, or a &lt;code&gt;Bun.serve&lt;/code&gt; fetch. Round robin load balancing, zero shared state, no session store to leak memory.&lt;/p&gt;

&lt;p&gt;That is a much simpler thing to build. It also means the engineering is no longer in the plumbing. It is in the contracts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three layers, three contracts
&lt;/h2&gt;

&lt;p&gt;Once the server is a pure function, a well built MCP plugin decomposes cleanly into three concerns that have nothing to do with each other:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transport&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HTTP, OAuth, CORS, lifecycle&lt;/td&gt;
&lt;td&gt;&lt;a href="https://maxhealth.tech/mcp-http" rel="noopener noreferrer"&gt;&lt;code&gt;@maxhealth.tech/mcp-http&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Surface&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What the user sees and clicks&lt;/td&gt;
&lt;td&gt;&lt;a href="https://maxhealth.tech/prefab" rel="noopener noreferrer"&gt;&lt;code&gt;@maxhealth.tech/prefab&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Skin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What it looks like, everywhere&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/max-network/brandc" rel="noopener noreferrer"&gt;&lt;code&gt;brandc&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each one is a contract rather than a framework. You can throw any of the three away and keep the other two, and that is the claim the rest of this article has to earn. If you want the surface layer from someone else, &lt;a href="https://mcpui.dev/" rel="noopener noreferrer"&gt;&lt;code&gt;@mcp-ui/client&lt;/code&gt;&lt;/a&gt; renders MCP Apps views in a React host and slots into the same place. If you want the skin from somewhere else, any stylesheet defining the same custom properties will do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: transport
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@maxhealth.tech/mcp-http&lt;/code&gt; is a framework agnostic MCP HTTP layer built on the Web Fetch API. It runs on Workers, Pages Functions, Deno Deploy, Bun, Node 18+, and anything Hono deploys to. It has been stateless first from day one, which is why the July revision needed no redesign here. Additions, yes. Redesign, no.&lt;/p&gt;

&lt;p&gt;Here is a complete authenticated MCP server on Cloudflare Workers:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createWorkerFetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;forwardBearer&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="s1"&gt;@maxhealth.tech/mcp-http&lt;/span&gt;&lt;span class="dl"&gt;'&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;McpServer&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="s1"&gt;@modelcontextprotocol/server&lt;/span&gt;&lt;span class="dl"&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="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createWorkerFetch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;authorizationServer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://auth.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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;server&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;McpServer&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&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;fetchFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;forwardBearer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;// register tools using fetchFn for upstream calls&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;server&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;That import line is worth a paragraph on its own. The first version of this article said a peer range of &lt;code&gt;&amp;gt;= 1.29.0&lt;/code&gt; "wants a beta bump", which undersold it twice. v2 went GA on 27 July, and it is a &lt;strong&gt;package split&lt;/strong&gt; rather than a version bump: the monolithic &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt; became &lt;code&gt;@modelcontextprotocol/server&lt;/code&gt; and siblings. So migrating means adding a new peer dependency rather than widening a range. &lt;code&gt;mcp-http&lt;/code&gt; 0.3.0 made that move, and because no export was removed, renamed or reshaped, and &lt;code&gt;McpServer&lt;/code&gt; is API compatible across the two majors, for consumers it came down to one import path.&lt;/p&gt;

&lt;p&gt;For those twenty lines you also get a working OAuth resource server. &lt;a href="https://datatracker.ietf.org/doc/html/rfc9728" rel="noopener noreferrer"&gt;RFC 9728&lt;/a&gt; protected resource metadata is served automatically, RFC 8414 authorization server metadata optionally, and unauthenticated calls get a proper 401 with a &lt;code&gt;WWW-Authenticate&lt;/code&gt; header pointing at that metadata, which is how a compliant client discovers where to go and log in. Expired tokens are rejected on &lt;code&gt;exp&lt;/code&gt; before any work happens, with a 30 second clock skew buffer. CORS is configurable, and an &lt;code&gt;onRequest&lt;/code&gt; hook reports outcome, status and duration per call.&lt;/p&gt;

&lt;p&gt;Two design decisions in there are worth stealing regardless of what you end up using.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;createServer&lt;/code&gt; is a per request factory, and it receives the caller's raw token.&lt;/strong&gt; That is the whole trick of stateless auth. The server instance lives exactly as long as the request that authorized it, so there is no shared instance for a token to leak through, and no window in which one user's credential is reachable from another user's call. It also means the implementation underneath is boring in the way you want security-adjacent code to be boring: construct a transport, connect the server, handle the request, throw it all away. There is no session id generator anywhere in the codebase, because there is nothing to correlate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;forwardBearer(token)&lt;/code&gt; is the seam for on-behalf-of calls.&lt;/strong&gt; It returns a &lt;code&gt;fetch&lt;/code&gt; that carries the caller's identity upstream, so your tools never handle a credential directly. They receive a function and call it. That sounds like a small ergonomic difference and it is actually the compliance story: in healthcare, every FHIR read has to be attributable to a specific human with specific scopes, and a tool that cannot see a token also cannot accidentally log one, widen one, or send one to the wrong host. The seam is the difference between a demo and something you can put in front of an auditor.&lt;/p&gt;

&lt;p&gt;There is also a &lt;code&gt;handleMcpPostStateful&lt;/code&gt; path, which keeps a transport alive across requests so the server can issue sampling calls. Be clear eyed about what that is now: it routes on &lt;code&gt;Mcp-Session-Id&lt;/code&gt; and mints sessions on &lt;code&gt;initialize&lt;/code&gt;, and both of those are gone in the new revision. As of 0.3.0 it is formally deprecated along with &lt;code&gt;SessionStore&lt;/code&gt; and the &lt;code&gt;stateful&lt;/code&gt; / &lt;code&gt;sessionTtlMs&lt;/code&gt; options, scheduled for removal in 0.4.0. Design so you do not need it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the new revision actually cost a transport
&lt;/h3&gt;

&lt;p&gt;Being honest about the gap is more useful than claiming there is none. Stateless was the right shape early and the shape held, so the forecast work was small: header and body mismatches now have to be rejected with &lt;code&gt;400&lt;/code&gt; and JSON-RPC &lt;code&gt;-32020&lt;/code&gt; (&lt;code&gt;HeaderMismatch&lt;/code&gt;), &lt;code&gt;GET&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; on the MCP endpoint should answer &lt;code&gt;405&lt;/code&gt; where they used to &lt;code&gt;404&lt;/code&gt; so old clients can tell "wrong era" from "wrong URL", and the default CORS config had to stop exposing &lt;code&gt;Mcp-Session-Id&lt;/code&gt; and &lt;code&gt;Last-Event-ID&lt;/code&gt;, two headers that no longer exist.&lt;/p&gt;

&lt;p&gt;Two things bit that were not on that list, and both are better lessons than anything that was.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first was CORS, and it was invisible from the desktop.&lt;/strong&gt; &lt;code&gt;MCP-Protocol-Version&lt;/code&gt; has been required on every MCP HTTP request since 2025-06-18, and it is not on the CORS safelist. Leave it out of &lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt; and every browser-hosted MCP client fails preflight and never reaches your endpoint, while Claude Desktop works perfectly the entire time because it is not a browser and never sends a preflight. That class of bug is worth internalising: any MCP server tested in exactly one host is a server that works in exactly one host. Fixed in 0.2.0, along with deriving &lt;code&gt;Access-Control-Allow-Methods&lt;/code&gt; per route (preflight was advertising &lt;code&gt;GET&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; while the handler answered those with &lt;code&gt;405&lt;/code&gt;) and admitting the per-tool &lt;code&gt;Mcp-Param-*&lt;/code&gt; headers that SEP-2243 introduces, whose names are chosen by the server per tool and so cannot be covered by a fixed list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The second was RFC 9728 itself.&lt;/strong&gt; §3.1 forms the metadata URL by inserting the well-known segment &lt;em&gt;between&lt;/em&gt; the host and the resource path, so an endpoint mounted at &lt;code&gt;/mcp&lt;/code&gt; publishes its metadata at &lt;code&gt;/.well-known/oauth-protected-resource/mcp&lt;/code&gt;. We served only the bare path and pointed the &lt;code&gt;WWW-Authenticate&lt;/code&gt; challenge there, which is off spec for any non-root mount point. It was found by diffing our behaviour against &lt;code&gt;@modelcontextprotocol/server@2.0.0&lt;/code&gt;, which implements the rule correctly, and that is a technique worth stealing too: when a reference implementation exists, run it beside yours and compare bytes.&lt;/p&gt;

&lt;p&gt;None of that touched anyone's tool logic. That part of the seam held.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: surface
&lt;/h2&gt;

&lt;p&gt;Now the interesting part. Your tool ran, you have data. What comes back?&lt;/p&gt;

&lt;p&gt;The default answer is a wall of text for the model to summarize. That is fine for a lookup and terrible for anything a human should touch. If MCP is about using applications &lt;em&gt;together with&lt;/em&gt; AI, then a tool result has to be able to be an application.&lt;/p&gt;

&lt;p&gt;That is what &lt;code&gt;@maxhealth.tech/prefab&lt;/code&gt; does. You build a component tree on the server and hand it to &lt;code&gt;display()&lt;/code&gt;:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;display&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;H1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;autoTable&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="s1"&gt;@maxhealth.tech/prefab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;listPatients&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;patients&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM patients&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nc"&gt;H1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Patients&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;autoTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;patients&lt;/span&gt;&lt;span class="p"&gt;)]),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Patients&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;&lt;code&gt;display()&lt;/code&gt; does two things at once, and the second is the one people miss. It serializes the tree to the &lt;code&gt;$prefab&lt;/code&gt; wire format and puts that JSON in &lt;code&gt;structuredContent&lt;/code&gt;, which is what the host renders. It &lt;em&gt;also&lt;/em&gt; puts a text form in &lt;code&gt;content[]&lt;/code&gt;, which is what the model reads. Those are different audiences with different needs, and a tool result has to satisfy both: hosts without UI support, and the model itself, still need something meaningful when the interface never paints.&lt;/p&gt;

&lt;p&gt;The rendering side is deliberately unglamorous. A zero dependency vanilla DOM renderer, loaded as a single script tag inside a &lt;code&gt;ui://&lt;/code&gt; resource, paints the tree in the host's sandboxed iframe. No bundler, no framework, no client build step in your project at all. What you get for that is 115+ components, reactive template expressions, and auto renderers (&lt;code&gt;autoTable&lt;/code&gt;, &lt;code&gt;autoChart&lt;/code&gt;, &lt;code&gt;autoForm&lt;/code&gt;, &lt;code&gt;autoMetrics&lt;/code&gt;) that turn raw rows into a real interface in one call.&lt;/p&gt;

&lt;p&gt;The mechanism underneath is &lt;strong&gt;MCP Apps&lt;/strong&gt;, and its status is worth being precise about. MCP Apps is not part of the core specification. It is an official &lt;em&gt;extension&lt;/em&gt;, identified as &lt;code&gt;io.modelcontextprotocol/ui&lt;/code&gt;, living in its own repository (&lt;code&gt;modelcontextprotocol/ext-apps&lt;/code&gt;) and versioning independently. It landed with protocol version &lt;code&gt;2026-01-26&lt;/code&gt;, which is still current even though core has moved on to &lt;code&gt;2026-07-28&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two corrections to the first draft here:&lt;/p&gt;

&lt;p&gt;I wrote that MCP Apps is "one of two official extensions". There are four: OAuth Client Credentials and Enterprise-Managed Authorization under &lt;code&gt;ext-auth&lt;/code&gt;, MCP Apps, and MCP Tasks. Tasks is the interesting one, because the 2026-07-28 revision is precisely what promoted it. Tasks used to be experimental inside core, and SEP-2663 moved them out into &lt;code&gt;io.modelcontextprotocol/tasks&lt;/code&gt; with a redesigned polling API.&lt;/p&gt;

&lt;p&gt;I also wrote that "the 2026-07-28 core revision does not mention Apps at all", but the revision's index page lists MCP Apps under Extensions, beside Tasks and Skills over MCP. What the revision added is the &lt;code&gt;extensions&lt;/code&gt; capability field that makes the separation official and negotiable.&lt;/p&gt;




&lt;p&gt;Wiring a tool to a UI takes two things: a registered viewer resource, and a pointer to it on the tool.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;registerViewerResource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PREFAB_RESOURCE_URI&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="s1"&gt;@maxhealth.tech/prefab/mcp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;registerViewerResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;browse&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="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Browse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;_meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;resourceUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PREFAB_RESOURCE_URI&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;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&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="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;structuredContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&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;Reach for &lt;code&gt;registerTool&lt;/code&gt; here rather than &lt;code&gt;tool()&lt;/code&gt;. Every &lt;code&gt;tool()&lt;/code&gt; overload is deprecated in favour of it, and only &lt;code&gt;registerTool&lt;/code&gt;'s config object accepts &lt;code&gt;inputSchema&lt;/code&gt; and &lt;code&gt;_meta&lt;/code&gt;. Note also where &lt;code&gt;_meta.ui.resourceUri&lt;/code&gt; sits: on the tool &lt;strong&gt;definition&lt;/strong&gt;, so it appears in &lt;code&gt;tools/list&lt;/code&gt; and the host can fetch and warm the template before the tool is ever called. Putting it on the result is the intuitive mistake, and it costs you that prefetch.&lt;/p&gt;

&lt;p&gt;That one-line helper exists because of three failures that each cost an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The MIME type is exactly &lt;code&gt;text/html;profile=mcp-app&lt;/code&gt;&lt;/strong&gt;, with no space after the semicolon. Plain &lt;code&gt;text/html&lt;/code&gt; is silently treated as an ordinary resource and never loads in an iframe. There is no error, the UI simply never appears.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSP has to go on both the resource listing and the content item.&lt;/strong&gt; The spec has hosts check both, preferring the content item and falling back to the listing. So the content item wins wherever both are set, which sounds like the listing is redundant, right up until you meet a host that only reads the content item and a policy declared solely on the listing is dropped. You get a black iframe and no explanation. Setting both is always safe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;structuredContent&lt;/code&gt; is required.&lt;/strong&gt; Return only &lt;code&gt;content[]&lt;/code&gt; and the host prints raw JSON, because the UI path never fires at all.&lt;/p&gt;

&lt;p&gt;As of prefab 0.3.6 that same call also declares the &lt;code&gt;io.modelcontextprotocol/ui&lt;/code&gt; capability on the server and emits the &lt;code&gt;ttlMs&lt;/code&gt; and &lt;code&gt;cacheScope&lt;/code&gt; the new revision expects. Which brings me to the part of my own argument that did not survive contact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the seam did not hold
&lt;/h3&gt;

&lt;p&gt;The original version of this article closed the transport section with "none of that touches your tools. That is the point of the seam." That was too strong, and the two exceptions are more interesting than the rule.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ttlMs&lt;/code&gt; and &lt;code&gt;cacheScope&lt;/code&gt; live on results, so they reached the surface layer directly. prefab's viewer HTML is a pure function of the package version, because the CDN base pins it, which means it is safely shared-cacheable and can ship &lt;code&gt;{ ttlMs: 86_400_000, cacheScope: 'public' }&lt;/code&gt;. Without that it inherits the conservative fallback and hosts re-fetch the viewer on every single render. That is a result-shaped change in a layer I had claimed the transport insulated.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;resultType&lt;/code&gt; looked like exactly the same problem and turned out not to be one. It is a wire level key: the SDK stamps it at its encode seam and strips it before results reach consumers, which is why its public result types omit it entirely. I was ready to add it by hand in five places, and doing so would have been wrong. The honest version of my original line is "none of that touches your tool &lt;em&gt;logic&lt;/em&gt;", which is weaker and true. The seam protects the handler body. It does not protect the envelope.&lt;/p&gt;

&lt;p&gt;The pattern that makes all of this compose is that every handler returns a self contained UI. A list view carries a button whose click calls a detail tool. The detail view carries one that opens an edit form. Submitting calls a save tool. Multi screen flows fall out of independent handlers with no client side router and no shared state, which is exactly what a stateless protocol wants. And when only the numbers change rather than the layout, &lt;code&gt;display_update()&lt;/code&gt; sends a state patch that the renderer merges into the live store instead of rebuilding the tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: skin
&lt;/h2&gt;

&lt;p&gt;Two layers in, there is a quiet duplication problem waiting. Your marketing site has a palette. Your web app has the same palette again in Tailwind config. Now your MCP UI needs it a third time as prefab wire JSON. Three copies of one brand in three vocabularies is three chances to drift, and they will drift silently, because nothing fails when they disagree.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;brandc&lt;/code&gt; is a brand compiler that solves this by separating two axes. &lt;strong&gt;The contract&lt;/strong&gt; is the &lt;em&gt;names&lt;/em&gt; of the variables (&lt;code&gt;--primary&lt;/code&gt;, &lt;code&gt;--card&lt;/code&gt;, &lt;code&gt;--radius&lt;/code&gt;, &lt;code&gt;--success&lt;/code&gt;, &lt;code&gt;--font-sans&lt;/code&gt;), stable across every brand and every stack. &lt;strong&gt;A brand&lt;/strong&gt; is the &lt;em&gt;values&lt;/em&gt;, with each colour authored once as a &lt;code&gt;{ light, dark }&lt;/code&gt; pair in structured TypeScript. Every delivery format is generated from that single source, so they cannot disagree: a plain CSS custom property stylesheet for SSR string injection, a &lt;code&gt;theme.css&lt;/code&gt; for bundlers, a Tailwind v4 &lt;code&gt;@theme inline&lt;/code&gt; preset that maps by reference so runtime dark switching keeps working, and a prefab wire theme.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;toPrefabTheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxhealth&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="s1"&gt;brandc&lt;/span&gt;&lt;span class="dl"&gt;'&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;display&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="s1"&gt;@maxhealth.tech/prefab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;toPrefabTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxhealth&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;Look at what makes that line work. &lt;code&gt;toPrefabTheme(brand)&lt;/code&gt; returns &lt;code&gt;{ light, dark }&lt;/code&gt; string maps. Prefab's &lt;code&gt;Theme&lt;/code&gt; interface accepts &lt;code&gt;{ light?, dark? }&lt;/code&gt; string maps. The two packages have no knowledge of each other, no shared dependency and no coordination, and they compose exactly, because both agreed on a shape instead of on an implementation. That is the whole thesis of this article in one function call.&lt;/p&gt;

&lt;p&gt;Rebranding is then a matter of supplying different values against the same contract, which is a data change rather than a code change. One gotcha if you do this in a Tailwind v4 app and want to rebrand colours only: pass an empty &lt;code&gt;scalars&lt;/code&gt;, because the contract's &lt;code&gt;--radius*&lt;/code&gt; and &lt;code&gt;--shadow*&lt;/code&gt; names collide with the keys Tailwind uses in &lt;code&gt;@theme&lt;/code&gt;, and emitting them overrides Tailwind's own scale.&lt;/p&gt;

&lt;p&gt;One more finding since publication, for anyone building their own token layer. brandc 0.6.0 stopped &lt;code&gt;@property&lt;/code&gt;-registering the scheme-dependent tokens. Registering a custom property forces its value to resolve at computed value time, which means a &lt;code&gt;light-dark()&lt;/code&gt; token declared on &lt;code&gt;:root&lt;/code&gt; decides the colour scheme &lt;em&gt;there&lt;/em&gt;, for the entire document, and a &lt;code&gt;.dark&lt;/code&gt; further down the tree silently does nothing to it. Native controls in that subtree still flip, so you get a light panel containing a dark input and no obvious cause. Leaving the scheme-dependent half unregistered is what lets a subtree theme itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole plugin
&lt;/h2&gt;

&lt;p&gt;Stacked up, an authenticated, branded, interactive MCP plugin is about thirty lines:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createWorkerFetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;forwardBearer&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="s1"&gt;@maxhealth.tech/mcp-http&lt;/span&gt;&lt;span class="dl"&gt;'&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;display&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;autoTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;H1&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="s1"&gt;@maxhealth.tech/prefab&lt;/span&gt;&lt;span class="dl"&gt;'&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;registerViewerResource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PREFAB_RESOURCE_URI&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="s1"&gt;@maxhealth.tech/prefab/mcp&lt;/span&gt;&lt;span class="dl"&gt;'&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;McpServer&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="s1"&gt;@modelcontextprotocol/server&lt;/span&gt;&lt;span class="dl"&gt;'&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;toPrefabTheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxhealth&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="s1"&gt;brandc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toPrefabTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxhealth&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="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createWorkerFetch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;authorizationServer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://auth.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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;server&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;McpServer&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;patients&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&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;fetchFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;forwardBearer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="nf"&gt;registerViewerResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;list_patients&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="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;List Patients&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;_meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;resourceUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PREFAB_RESOURCE_URI&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;async &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;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://fhir.example.com/Patient&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nc"&gt;H1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Patients&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;autoTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)]),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Patients&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&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;return&lt;/span&gt; &lt;span class="nx"&gt;server&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;No session store. No sticky routing. No handshake to keep alive. Nothing in memory between requests. It scales behind a round robin balancer because there is nothing to scale, and it was already the shape the July 2026 revision asks for before that revision was written.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question that follows every demo
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;authorizationServer: 'https://auth.example.com'&lt;/code&gt; quietly hides the thing people ask about thirty seconds after seeing this run. What does that server actually have to be?&lt;/p&gt;

&lt;p&gt;Four things. Your server needs RFC 9728 protected resource metadata, so a client can discover which authorization server guards you. That server needs RFC 8414 metadata at &lt;code&gt;/.well-known/oauth-authorization-server&lt;/code&gt;, so its endpoints and grants are discoverable rather than configured by hand. It needs a client onboarding story, meaning Client ID Metadata Documents now or DCR while it lasts, because without one every consumer has to be hand-registered by you. And it needs scopes that can express something in your domain, since those scopes are the boundary of what you are actually selling.&lt;/p&gt;

&lt;p&gt;All four are checkable in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nv"&gt;$ISSUER&lt;/span&gt;/.well-known/oauth-authorization-server &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'{registration_endpoint, grant_types_supported, scopes_supported}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 404 on that URL is itself the answer. Plenty of perfectly good OIDC identity providers serve only &lt;code&gt;/.well-known/openid-configuration&lt;/code&gt;, expose no &lt;code&gt;registration_endpoint&lt;/code&gt;, and grant &lt;code&gt;authorization_code&lt;/code&gt; alone. That signs users in and does it well. Authorizing a plugin is a different job, and pointing &lt;code&gt;discoverAuthorizationServer&lt;/code&gt; at one gets you a 404 rather than a useful error.&lt;/p&gt;

&lt;h2&gt;
  
  
  So is software engineering dead?
&lt;/h2&gt;

&lt;p&gt;The typing is dying. Good.&lt;/p&gt;

&lt;p&gt;What is left is the part that was always the actual job: deciding where the seams go. &lt;code&gt;createServer&lt;/code&gt; taking a token instead of closing over one. &lt;code&gt;display()&lt;/code&gt; returning an envelope where a string would have done. &lt;code&gt;toPrefabTheme()&lt;/code&gt; returning a shape another package happens to accept. None of those are hard to write, and a model will write any of them for you in seconds. Knowing that they are the three cuts that let a plugin survive a protocol rewrite is the work.&lt;/p&gt;

&lt;p&gt;It is worth saying plainly what updating this article demonstrated, because it is a more useful result than the original claim. Of the three seams, the transport one held completely: a package rename, a CORS header, a well-known path. The surface seam leaked, because &lt;code&gt;ttlMs&lt;/code&gt; and &lt;code&gt;cacheScope&lt;/code&gt; are result-shaped and results are the thing the surface layer builds. Contracts do not make you immune to a spec revision. They make the blast radius small enough to fix in an afternoon, and they mean you hear about it from a changelog instead of from your error rate.&lt;/p&gt;

&lt;p&gt;The protocol will change again on some Tuesday. Build against contracts and that will be a Tuesday you can plan for.&lt;/p&gt;




&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/changelog" rel="noopener noreferrer"&gt;MCP specification 2026-07-28: key changes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/extensions/overview" rel="noopener noreferrer"&gt;Extensions overview and negotiation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://modelcontextprotocol.io/extensions/apps/overview" rel="noopener noreferrer"&gt;MCP Apps extension&lt;/a&gt; and its &lt;a href="https://github.com/modelcontextprotocol/ext-apps" rel="noopener noreferrer"&gt;repository&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http" rel="noopener noreferrer"&gt;Streamable HTTP transport, 2026-07-28&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/community/feature-lifecycle" rel="noopener noreferrer"&gt;Feature lifecycle and deprecation policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575" rel="noopener noreferrer"&gt;SEP-2575: Make MCP Stateless&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322" rel="noopener noreferrer"&gt;SEP-2322: Multi round trip requests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@modelcontextprotocol/server" rel="noopener noreferrer"&gt;&lt;code&gt;@modelcontextprotocol/server&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@maxhealth.tech/mcp-http" rel="noopener noreferrer"&gt;&lt;code&gt;@maxhealth.tech/mcp-http&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://maxhealth.tech/prefab" rel="noopener noreferrer"&gt;&lt;code&gt;@maxhealth.tech/prefab&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/brandc" rel="noopener noreferrer"&gt;&lt;code&gt;brandc&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>llm</category>
      <category>mcp</category>
    </item>
    <item>
      <title>DRY: Don't Repeat Yourself</title>
      <dc:creator>Maximilian Nussbaumer</dc:creator>
      <pubDate>Wed, 17 Jun 2026 19:07:51 +0000</pubDate>
      <link>https://dev.to/quotentiroler/dry-dont-repeat-yourself-24ag</link>
      <guid>https://dev.to/quotentiroler/dry-dont-repeat-yourself-24ag</guid>
      <description>&lt;p&gt;When we wrote code before large language models could write it for us, code duplication was rarely our problem, at least not if we worked alone or in a small team.&lt;/p&gt;

&lt;p&gt;That has changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A principle born in a quieter time
&lt;/h2&gt;

&lt;p&gt;"Don't Repeat Yourself" was coined by Andy Hunt and Dave Thomas in The Pragmatic Programmer back in 1999, a year when our biggest worry was whether the clocks would survive January. The idea is reassuringly simple. Every piece of knowledge in your system should have a single, unambiguous home. Write it once, point at it from everywhere else, go to lunch.&lt;/p&gt;

&lt;p&gt;For those of us working alone or in small teams, DRY was almost automatic. You held the whole codebase in your head, all of it, including the parts you were ashamed of. When you needed to format a date, you remembered you already had a formatDate helper, because you had written it last Tuesday and were still slightly proud of it. Duplication was something that happened to other people, specifically large organizations where three teams could write three subtly different currency formatters and never once meet in a corridor.&lt;/p&gt;

&lt;p&gt;Duplication, in other words, was a problem that scaled with headcount. The more humans you added, the more copies of everything you got.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now everyone gets to have the problem
&lt;/h2&gt;

&lt;p&gt;Here is the shift. An LLM writing code for you behaves rather like a brand new contractor who has never seen your project, is enormously eager to help, and has politely forgotten everything the moment they finish each task.&lt;/p&gt;

&lt;p&gt;It does not know that formatDate already exists. It does not know your team solemnly agreed that all network calls go through apiClient. It does not know that the precise validation logic it is about to invent is sitting, fully tested, three folders away. So it does the friendly thing and writes some fresh code that works.&lt;/p&gt;

&lt;p&gt;And it does work. The tests pass, the feature ships, the little green checkmark appears, everyone is delighted. You simply now own a fourth date formatter, a second and mildly incompatible API wrapper, and a button component that matches your design system the way a cover band matches the original.&lt;/p&gt;

&lt;p&gt;The duplication problem used to scale with the number of people. It now scales with the number of generations. A single developer with an AI assistant can produce, before lunch, the sort of sprawl that once required an entire floor of the building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why models duplicate without meaning to
&lt;/h2&gt;

&lt;p&gt;It helps to understand why this happens, mostly because it points directly at the fix. There are really three reasons, and none of them involve the model being foolish.&lt;/p&gt;

&lt;p&gt;The first is context. The model only sees what you hand it, and your 200-file project does not fit through the letterbox. It cannot reuse a helper it has never been allowed to look at. The second is incentive. Generating new code is the thing models are good at and rewarded for, whereas reusing old code first requires finding the old code, an activity most setups make about as convenient as it sounds. The third is more human than you would expect. A self-contained block that compiles simply feels finished, and nothing about "finished" nudges anyone, silicon or otherwise, to go hunting for the canonical version that already exists.&lt;/p&gt;

&lt;p&gt;Notice that all three are about memory, not intelligence. The model is not failing to reason. It is reasoning brilliantly about the tiny slice of the world you managed to fit in front of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it quietly costs you
&lt;/h2&gt;

&lt;p&gt;Duplication is not merely untidy. It is where bugs go to retire comfortably.&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="c1"&gt;// payments/checkout.ts&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cents&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="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`$&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;cents&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="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;// invoices/render.ts, written by an agent two weeks later&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;priceToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&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="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`$&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// same idea, fresh coat of paint&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These two look harmless, almost charming. Then you launch in Europe, need euros and comma decimals, and confidently patch formatPrice. The invoices keep cheerfully printing dollars, because nobody alive remembers that priceToString exists. You have just shipped a production bug whose root cause is a duplication that no human ever actually decided to create.&lt;/p&gt;

&lt;p&gt;Now run that forward across a year of helpful AI commits. The bill arrives as inconsistent behavior, as maintenance that has to be performed several times in several places by someone who can find all of them, and as the slow erosion of your belief that the codebase has one correct way to do anything. It stops having one, so the belief was right to leave.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do about it
&lt;/h2&gt;

&lt;p&gt;You do not fix this by asking everyone to be more careful, which is the programming equivalent of asking the weather to be nicer. You fix it by giving whatever writes the code a way to know what already exists.&lt;/p&gt;

&lt;p&gt;That is the entire reason I built &lt;a href="https://dry.codes" rel="noopener noreferrer"&gt;DRY.codes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;DRY.codes indexes the repositories you connect, across your personal account and your organizations, and builds two things from them. The first is a fast, searchable memory of every file you have already written. The second is a precomputed map of where your code already repeats itself, file pairs ranked by how much they actually share. It then exposes all of it over the Model Context Protocol, which is the part that matters, because MCP is the letterbox through which your AI assistant finally gets to see the rest of the house.&lt;/p&gt;

&lt;p&gt;So the contractor who forgets everything gets a memory. Before writing a new helper, an agent can search the knowledge base for one that already does the job. It can locate a file by name, compare two files to decide whether a near-duplicate is worth consolidating, and run a snippet through dry_wand to ask, in effect, "have we written this already?" The answer arrives before the duplicate does, which is the only time the answer is useful.&lt;/p&gt;

&lt;p&gt;You can point it at a single repository or an entire organization to see the worst duplication hotspots and decide what to refactor first. You can keep shared configuration and utilities honest across dozens of projects at once. And if you live in Claude Code, the plugin wires this straight into your workflow with a "reuse before you write" reflex, so the search happens by default rather than by virtue. There is even a public corpus at dry.codes/mcp/public if you want to feel the idea before connecting anything of your own.&lt;/p&gt;

&lt;p&gt;The point is not to scold the model. The point is to hand it the one thing it was missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cheerful twist
&lt;/h2&gt;

&lt;p&gt;DRY did not become less important when machines learned to write code. It became more important, and it became democratic. The duplication problem that used to belong exclusively to large organizations now belongs to anyone with an AI assistant and an afternoon.&lt;/p&gt;

&lt;p&gt;The happy part is that the same tools generating all this duplication are perfectly capable of avoiding it, the moment you give them some memory of the code that is already sitting right there.&lt;/p&gt;

&lt;p&gt;Write it once. Then make sure everything, human and machine alike, can actually find it. That last sentence used to be advice. With DRY.codes, it is just how the tooling works.&lt;/p&gt;

&lt;p&gt;Follow &lt;a href="https://github.com/quotentiroler" rel="noopener noreferrer"&gt;quotentiroler on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>coding</category>
      <category>programming</category>
      <category>productivity</category>
      <category>automation</category>
    </item>
    <item>
      <title>Why MCP Apps are going to be the next big thing</title>
      <dc:creator>Maximilian Nussbaumer</dc:creator>
      <pubDate>Mon, 04 May 2026 22:48:11 +0000</pubDate>
      <link>https://dev.to/quotentiroler/why-mcp-apps-are-going-to-be-the-next-big-thing-3611</link>
      <guid>https://dev.to/quotentiroler/why-mcp-apps-are-going-to-be-the-next-big-thing-3611</guid>
      <description>&lt;p&gt;There's a quiet shift happening in how AI tools interact with users, and most developers haven't noticed yet, because just one week after the MCP Apps specs were published by Anthropic, OpenAI launched a huge marketing campaign around OpenClaw and they got all the attention, at least for a while.&lt;/p&gt;

&lt;p&gt;For the past two years, every AI assistant has been stuck behind the same interface: a text box. You ask a question, you get text back. Maybe some markdown. Maybe a code block. Maybe an image.&lt;/p&gt;

&lt;p&gt;Claude, GPT, Copilot, Gemini, every local model render into the same narrow pipe. MCP Apps change that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MCP Apps actually is
&lt;/h2&gt;

&lt;p&gt;MCP Apps is a protocol extension that lets MCP tool results include interactive UIs. Actual interactive components running inside the AI host's sandbox.&lt;/p&gt;

&lt;p&gt;The mechanics are straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your MCP tool returns a &lt;code&gt;structuredContent&lt;/code&gt; payload alongside the normal text &lt;code&gt;content&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The host loads a &lt;code&gt;ui://&lt;/code&gt; resource (an HTML page you provide) into a sandboxed iframe&lt;/li&gt;
&lt;li&gt;The host forwards the tool result to your iframe via &lt;code&gt;postMessage&lt;/code&gt; using a JSON-RPC 2.0 protocol (&lt;code&gt;ui/*&lt;/code&gt; methods)&lt;/li&gt;
&lt;li&gt;Your renderer mounts the UI inside the iframe&lt;/li&gt;
&lt;li&gt;The UI can call tools back, send messages to the conversation, request display mode changes, and resize itself&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The text &lt;code&gt;content&lt;/code&gt; still serves LLM reasoning and hosts without rendering support. The UI is a progressive enhancement. Let's be honest: natural language is not always the most convenient or fastest way to express a desire. A click or a tap on a button can be much faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spec behind it
&lt;/h2&gt;

&lt;p&gt;The MCP Apps protocol (spec version &lt;code&gt;2026-01-26&lt;/code&gt;) is an official extension to MCP maintained by Anthropic. It defines a JSON-RPC 2.0 message layer over &lt;code&gt;postMessage&lt;/code&gt; between a sandboxed iframe and its host. The spec covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Handshake&lt;/strong&gt;: &lt;code&gt;ui/initialize&lt;/code&gt; request/response with protocol version negotiation, app capabilities, and host capabilities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifecycle&lt;/strong&gt;: &lt;code&gt;tool-input&lt;/code&gt;, &lt;code&gt;tool-result&lt;/code&gt;, &lt;code&gt;tool-cancelled&lt;/code&gt;, &lt;code&gt;tool-input-partial&lt;/code&gt; (streaming), &lt;code&gt;host-context-changed&lt;/code&gt;, &lt;code&gt;resource-teardown&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actions&lt;/strong&gt;: &lt;code&gt;ui/open-link&lt;/code&gt;, &lt;code&gt;ui/message&lt;/code&gt;, &lt;code&gt;ui/request-display-mode&lt;/code&gt;, &lt;code&gt;ui/update-model-context&lt;/code&gt;, &lt;code&gt;tools/call&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sizing&lt;/strong&gt;: &lt;code&gt;ui/notifications/size-changed&lt;/code&gt; (reactive, from app to host), &lt;code&gt;ui/notifications/preferred-size&lt;/code&gt; (declarative hints)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: CSP via &lt;code&gt;_meta.ui.csp&lt;/code&gt; on resource content items, Permission Policy for camera/microphone/geolocation/clipboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The spec is deliberately renderer-agnostic. It defines how the host and the iframe talk to each other. What you put inside the iframe is entirely your choice. The host doesn't parse your component tree or validate your DOM structure. It sends you JSON-RPC messages and expects JSON-RPC messages back.&lt;/p&gt;

&lt;p&gt;This is a deliberate design decision and the reason multiple rendering approaches can coexist. The Anthropic ext-apps SDK, a prefab renderer, a raw React app, a Svelte component -- all valid. The protocol doesn't care.&lt;/p&gt;

&lt;p&gt;Current host implementations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Host&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VS Code Copilot Chat&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shipping&lt;/td&gt;
&lt;td&gt;Full spec support, CSP via &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tag, &lt;code&gt;acquireVsCodeApi()&lt;/code&gt; transport&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Desktop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shipping&lt;/td&gt;
&lt;td&gt;Full spec support, CSP via HTTP headers on sandboxed origin (&lt;code&gt;{hash}.claudemcpcontent.com&lt;/code&gt;). Can send results before init completes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ChatGPT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shipping&lt;/td&gt;
&lt;td&gt;Full spec support, CSP via HTTP headers on sandboxed origin (&lt;code&gt;{slug}.oaiusercontent.com&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three independent hosts shipping the same protocol make apparent that this is not a proposal, but infrastructure.&lt;/p&gt;

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

&lt;p&gt;Here's the thing I think people are going to underestimate: &lt;strong&gt;this turns every MCP server into a full-stack application.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An MCP server is already a backend that exposes typed tools over stdio or HTTP. It already has access to databases, APIs, file systems. The only thing missing was a frontend.&lt;/p&gt;

&lt;p&gt;Now it has on that lives &lt;strong&gt;inside the AI conversation&lt;/strong&gt;, gets tool arguments and results pushed to it automatically, can call tools back on its own MCP server via the host, inherits the host's theme for free, and works across VS Code, Claude Desktop, ChatGPT, and anything else that implements the spec.&lt;/p&gt;

&lt;p&gt;That last point is the critical one. &lt;strong&gt;Write once, render everywhere.&lt;/strong&gt; The protocol is the same across hosts. The sandboxing model is the same. The postMessage bridge is the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two different things driven by the same idea
&lt;/h2&gt;

&lt;p&gt;MCP Apps is going to do to AI tooling what PWAs were supposed to do to mobile apps. PWAs were competing against native apps that users already loved. MCP Apps are filling a vacuum. There is no existing standard for rendering interactive UIs inside AI conversations. The alternative is pasting JSON into the chat. MCP Apps are going to be "AI Native Apps".&lt;/p&gt;

&lt;p&gt;The bar is low. And the protocol is good enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;

&lt;p&gt;Here's a complete MCP tool that returns an interactive patient list with search, sorting, and click-to-view detail:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;display&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;autoTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;H1&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="s1"&gt;@maxhealth.tech/prefab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;listPatients&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&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;patients&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM patients&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nc"&gt;H1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Patients&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nf"&gt;autoTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;patients&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="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Patient List&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;display()&lt;/code&gt; wraps the component tree into the MCP wire format. The host renders it. The table is interactive -- search, sort, row selection -- without writing any frontend code.&lt;/p&gt;

&lt;p&gt;But you don't even need to compose components. The auto-renderers infer the right UI from your data shape:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;display&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="s1"&gt;@maxhealth.tech/prefab/mcp&lt;/span&gt;&lt;span class="dl"&gt;'&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;autoTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;autoChart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;autoForm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;autoMetrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;autoDetail&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="s1"&gt;@maxhealth.tech/prefab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Array of objects -&amp;gt; searchable, sortable table&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;autoTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;patients&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// Array with numeric fields -&amp;gt; line/bar chart with axes and tooltips&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;autoChart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;salesData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;xAxis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;month&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Revenue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="c1"&gt;// Schema fields -&amp;gt; validated form that submits back to an MCP tool&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;autoForm&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Role&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;select&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;options&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="s1"&gt;admin&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="s1"&gt;user&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;save_user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// Key-value object -&amp;gt; formatted detail card&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;autoDetail&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lastSeen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-04-30&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="c1"&gt;// Object with numeric values -&amp;gt; metric cards with labels&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;autoMetrics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;patients&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1284&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;appointments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;waitTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;12min&lt;/span&gt;&lt;span class="dl"&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 auto-renderer picks columns, axes, labels, and formatting based on what it finds in the data. You pass an array or object, you get a production-quality UI back. When you outgrow them, you drop down to the component API and build exactly what you want.&lt;/p&gt;

&lt;p&gt;And when you want to tweak the look without writing full custom components, every element accepts utility classes:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;display&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;H1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Badge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;autoTable&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="s1"&gt;@maxhealth.tech/prefab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cssClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p-6 max-w-4xl&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="nc"&gt;H1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Patient Dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Badge&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;47 active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cssClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-sm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;autoTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;patients&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cssClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rounded-lg shadow-md&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="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preferredHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&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 built-in CSS ships ~200 Tailwind-compatible utility classes (padding, margin, flex, grid, gap, typography, colors, borders, shadows, max-height, overflow). No Tailwind dependency, no build step, no purge config. They're there in the 15KB stylesheet that the CDN serves alongside the renderer.&lt;/p&gt;

&lt;p&gt;Three levels of control: auto-renderers for zero-config, utility classes for visual tweaks, full component API for complete custom UIs.&lt;/p&gt;

&lt;p&gt;The HTML renderer is only 80kB in size and imported by a single script tag:&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;id=&lt;/span&gt;&lt;span class="s"&gt;"root"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/@maxhealth.tech/prefab@0.2/dist/renderer.auto.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The protocol is the product
&lt;/h2&gt;

&lt;p&gt;The real value is the protocol itself.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ui/initialize&lt;/code&gt; handshake. &lt;code&gt;ui/notifications/tool-result&lt;/code&gt; for pushing data. &lt;code&gt;ui/notifications/size-changed&lt;/code&gt; for responsive layout. &lt;code&gt;ui/open-link&lt;/code&gt; for navigation. &lt;code&gt;ui/message&lt;/code&gt; for sending messages back into the conversation. All JSON-RPC 2.0 over postMessage.&lt;/p&gt;

&lt;p&gt;Anyone can build a renderer against this protocol. React, Svelte, vanilla JS, raw DOM manipulation. The host sends JSON-RPC messages to an iframe and expects JSON-RPC messages back. Your rendering stack is your business.&lt;/p&gt;

&lt;p&gt;This is why I think MCP Apps will win where similar attempts failed. It's a host protocol that allows any rendering approach, and that makes adoption straightforward on both sides.&lt;/p&gt;

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

&lt;p&gt;It's early. The spec is dated 2026-01-26. Here's what's still rough:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSP implementation varies by host.&lt;/strong&gt; All hosts read &lt;code&gt;_meta.ui.csp&lt;/code&gt; from the content item returned by &lt;code&gt;readResource&lt;/code&gt;, but the enforcement mechanism differs. VS Code injects a &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tag. Claude Desktop and ChatGPT enforce via HTTP headers on a sandboxed origin. The spec standardizes the declaration format, but you should test your CSP config against each host you target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No standard component format.&lt;/strong&gt; The protocol defines the transport, the payload is up to you. Every renderer invents its own component schema. (We use a &lt;code&gt;$prefab&lt;/code&gt; JSON wire format, but nothing stops someone from using entirely different components.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission Policy support varies.&lt;/strong&gt; Camera, microphone, geolocation, clipboard-write access via iframe &lt;code&gt;allow&lt;/code&gt; attribute. Hosts report what they support in &lt;code&gt;hostCapabilities.sandbox.permissions&lt;/code&gt;, but not all hosts honor all permissions yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buffering and timing are tricky.&lt;/strong&gt; Claude Desktop can send &lt;code&gt;tool-result&lt;/code&gt; before the &lt;code&gt;ui/initialize&lt;/code&gt; response arrives. If your renderer doesn't buffer, you lose the first result. This took us several hours to debug.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are solvable problems. The architecture is sound.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prediction
&lt;/h2&gt;

&lt;p&gt;Within 12 months, we will see a market emerging around MCP Apps.&lt;/p&gt;

&lt;p&gt;MCP Apps hosts will compete on rendering quality, theme support, and permission handling. MCP servers will compete on UI polish. And the protocol will quietly become the standard that holds it all together. MCP Hosts potentially replace the need for an internet browser.&lt;/p&gt;




&lt;p&gt;Follow &lt;a href="https://maxhealth.tech" rel="noopener noreferrer"&gt;Max Health&lt;/a&gt; on &lt;a href="https://github.com/Max-Health-Inc" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>BDFL: Benevolent Dictator for Lobster - or - How I met Sam Altman</title>
      <dc:creator>Maximilian Nussbaumer</dc:creator>
      <pubDate>Mon, 16 Feb 2026 08:05:47 +0000</pubDate>
      <link>https://dev.to/quotentiroler/bdfl-benevolent-dictator-for-lobster-or-how-i-met-sam-altman-1k4m</link>
      <guid>https://dev.to/quotentiroler/bdfl-benevolent-dictator-for-lobster-or-how-i-met-sam-altman-1k4m</guid>
      <description>&lt;p&gt;What happens if you vibecode an open-source personal assistant that gets hyped? OpenAI might offer you a job and launch their biggest and most viral marketing campaign ever. Let me explain.&lt;/p&gt;

&lt;p&gt;When the creator of OpenClaw, Peter Steinberger, came to San Francisco, the party at Frontier Tower was huge. They ordered lobster rolls and crabs for everyone. I had no chance to talk to him, but I met him on the next day at the OpenAI Codex Hackathon, where he was sitting together with some attendees. What I saw then concerned me: Within a few minutes, he gave out maintenance access to some people he never met before (including me), telling us to bring down the PR count. No onboarding, no guidance, no staging. One task: Merge PRs into main. When someone asked him a question, he had no time or patience to answer. This immediately showed signs of a toxic working environment. Heads were hiding behind the screens, the fear of being cut out took over within no time.&lt;/p&gt;

&lt;p&gt;It is probably meant to be a bad joke that in the contribution guideline, there is no proper guidance on how to contribute. Instead, it shows the project owner as the "benevolent dictator". That's the credits Peter Steinberger &lt;a href="https://github.com/openclaw/openclaw/commit/f4a1190bdd6dc553d918ace8c7b599096723b077" rel="noopener noreferrer"&gt;gave to himself on January 2nd&lt;/a&gt;. What a New Year's resolution.&lt;/p&gt;

&lt;p&gt;The benevolent dictator is a &lt;a href="https://guidebook.theopensourceway.org/growing-contributors/project-and-community-governance" rel="noopener noreferrer"&gt;well-known pattern&lt;/a&gt; when there is a huge hype around one individual, and the "best practice" way to handle that situation is to re-assign authority to a board of people. It is truly difficult but necessary to handle this well, in order to minimize harmful consequences. "The best-case outcome is a BDFL who recognizes when to transition to a broader governance model before the drawbacks cause real damage." BDFL works when the project is small enough or the (benevolent) dictator is skilled enough to maintain genuine oversight. Neither is the case.&lt;/p&gt;

&lt;p&gt;I walked away from that table where Peter was sitting at and met many other people instead. I met Sam Altman, but this was most likely not a coincidence. That hackathon was just the kick-off of their marketing campaign. Codex 5.3 was released on that day, and they want it to be used. Anthropic dominates code generation, Google dominates image generation. They grasp at every straw, trying to stay above water. OpenClaw is the card OpenAI played, and they bet a lot on it. Peter consistently promotes Codex over competing tools and dismisses MCP, and on February 15th it became official: Peter joins OpenAI as an employee.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9tghj5jkwi30m3yvpnzo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9tghj5jkwi30m3yvpnzo.png" alt="Ramp AI Index Feb 2026" width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nevertheless, I accepted the invite to become an (unpaid) maintainer of OpenClaw. The first thing I did: I asked for staging after I saw that every PR gets directly merged into the main codebase, which is like turning screws &lt;a href="https://www.reddit.com/r/explainlikeimfive/comments/1kob5xw/eli5_why_is_pushing_to_a_main_branch_bad_what_is/" rel="noopener noreferrer"&gt;on an airplane that is about to take off&lt;/a&gt;. It's obvious why that is considered "bad practice".&lt;/p&gt;

&lt;p&gt;But Peter had no time to think about it. A few days later, I was the top contributor, and people started reaching out to me via email, pinging me in their PRs, asking for reviews, offering jobs, even on X! Not because I implemented staging or shipped features. Because I removed garbage. "&lt;em&gt;Your contributions to openclaw/openclaw caught my attention and I wanted to reach out&lt;/em&gt;."&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;Over tens of thousands GitHub Actions minutes burned in a few days. Failed builds, redundant test runs, CI churn from missing gates. I found many files with thousands of lines of code, and hundreds of duplicate functions across the codebase. Both identical functions and slight variations of the same logic, written by AI agents that had zero awareness of what already existed. No human actually knows this codebase. It is being vibecoded with minimal human oversight.&lt;/p&gt;

&lt;h3&gt;
  
  
  How This Happens
&lt;/h3&gt;

&lt;p&gt;It's a simple, cancerous loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AI agent gets a task&lt;/li&gt;
&lt;li&gt;It writes code that "works", meaning it passes the immediate test and satisfies the prompt&lt;/li&gt;
&lt;li&gt;Nobody checks whether the code duplicates existing functionality&lt;/li&gt;
&lt;li&gt;Nobody notices the file went from 3,000 lines to 5,300&lt;/li&gt;
&lt;li&gt;It gets merged&lt;/li&gt;
&lt;li&gt;Repeat a thousand times&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The LLM does not know that a nearly identical helper function already exists 1,000 lines above, or in the next module over, if it is not told to look for it. It solves the problem in front of it and moves on. Like a living cell that reproduces without control. When you put the human out of the loop, the codebase grows in volume, but decays in quality. The only critic is now satisfaction. "Did I get what I wanted?" If not, then the AI has to iterate further until I get what I wanted.&lt;/p&gt;

&lt;p&gt;Security concerns are more than valid when people tend to value hype over a reality check. If you wonder how OpenClaw is being released: Once every few days, Peter takes whatever code is currently in the main branch, and creates a new release. Sometimes he writes "YOLO" in the maintainer chat while doing it.&lt;/p&gt;

&lt;p&gt;Like the other day, when a maintainer bulk-merged a couple of PRs into main, Peter released it, cron jobs broke, and Elon Musk made fun of OpenClaw on X. Welcome to OpenClaw!&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Did
&lt;/h3&gt;

&lt;p&gt;The boring stuff nobody wants to do. I went through the codebase and identified duplicate and near-duplicate functions. Consolidated shared logic into reusable helpers. Broke apart files that had no business being thousands of lines long. Set up a quality gate so new submissions actually get reviewed for duplication and structural fit before they land on main. Enabled typechecking for modules. The codebase got smaller and more maintainable and CI stopped choking on broken builds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwecnzgvvcus75adz2req.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwecnzgvvcus75adz2req.png" alt="Top Contributors OpenClaw" width="800" height="535"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Response
&lt;/h3&gt;

&lt;p&gt;When I asked for staging, I noticed why a toxic working environment should be avoided: People are scared to say the opposite of what the benevolent dictator will say then. Being rejected by the benevolent dictator will devalue the position of the proposer, so there is no proposal against the narrative, and the narrative is "we accept AI generated code of any kind". &lt;/p&gt;

&lt;p&gt;When I put stricter quality gates in place, some people were not happy about it. Velocity matters more than quality. Ship fast, do not clean up. I see this pattern forming across the AI-assisted open source ecosystem. The pressure to merge PRs and ship features actively punishes the people doing real code maintenance work.&lt;/p&gt;

&lt;p&gt;I also gave credits to the contributors based on their actual contributions. Another maintainer, who &lt;a href="https://github.com/mercurai/openclaw/pull/1#issuecomment-3880816954" rel="noopener noreferrer"&gt;attacks people for pinging him on github&lt;/a&gt; or &lt;a href="https://github.com/openclaw/openclaw/pull/13863#issuecomment-3893306671" rel="noopener noreferrer"&gt;permabans them from contributing&lt;/a&gt;,  reverted the change and gave me the choice to shut up or leave. I thought about it for a day, and decided to leave and share my experiences.&lt;/p&gt;

&lt;p&gt;2 days later, the code quality gate I installed which prevented new duplicate functions from being introduced and code files from growing endlessly has already fallen. In &lt;a href="https://github.com/openclaw/openclaw/commit/c2178e252299bf40e2e412bb8944bbc4094011b4" rel="noopener noreferrer"&gt;commit/c2178..&lt;/a&gt;, it was removed for being "useless", when the gate has already saved thousands of CI minutes while preventing duplicate code from being merged.&lt;/p&gt;

&lt;p&gt;Peter's take? Well, he did not respond at all, which is typical for a benevolent dictator. Our only conversation took about 2 minutes, when he gave me maintenance access. Still, on February 14th, he assigned an army of agents both locally and in the cloud to continue with the refactorings and deduplications I started.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkealz2av6zyfrqlulmxp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkealz2av6zyfrqlulmxp.png" alt="Peter's sweep" width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Numbers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Snapshot&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Files&lt;/th&gt;
&lt;th&gt;Total LOC&lt;/th&gt;
&lt;th&gt;Duplicate function names&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Before CI gate removal&lt;/td&gt;
&lt;td&gt;Feb 12&lt;/td&gt;
&lt;td&gt;3,840&lt;/td&gt;
&lt;td&gt;694,527&lt;/td&gt;
&lt;td&gt;415&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Before Peter's sweep&lt;/td&gt;
&lt;td&gt;Feb 13 (end of day)&lt;/td&gt;
&lt;td&gt;4,052&lt;/td&gt;
&lt;td&gt;731,516&lt;/td&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After sweep&lt;/td&gt;
&lt;td&gt;Feb 14&lt;/td&gt;
&lt;td&gt;4,166&lt;/td&gt;
&lt;td&gt;741,598&lt;/td&gt;
&lt;td&gt;419&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the duplicates &lt;strong&gt;went up&lt;/strong&gt; from 415 → 429 between Feb 12-13 (new code was being added faster than dedup was happening), and Peter's sweep on Feb 14th brought it back down from 429 → 419, meaning new duplicates are being introduced while Peter is cleaning them up. He's swimming against the current, but his approach is not sustainable. It is just not possible to outsource all thinking to multi-agent systems. The consequence is digital cancer.&lt;/p&gt;

&lt;p&gt;Instead of wasting more tokens and energy, Peter could just re-enable the simple but highly effective code quality gate and pay naturally intelligent humans to clean up. In fact, Peter's artificially intelligent agent swarm is still refactoring non-stop, and brought duplicate functions down to 378. That took him 637 commits, which is very inefficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bigger Question
&lt;/h3&gt;

&lt;p&gt;I believe AI should be more accessible to everyone. That is the stated mission of projects like this. But accessibility without oversight produces exactly what I found: AI-made codebases that are not maintainable without AI. AI making humans depend on it. This might be the first step of AI taking over.&lt;/p&gt;

&lt;p&gt;AI does not need a dictator and his loyal doormats. It needs real human oversight and people who speak up. This project is dangerous, not because of AI, but because hype is dangerous, benevolent dictators, and people in charge riding the hype train while hiding behind pseudonyms. All world plays along, branding events and articles with crabs and lobsters, just because it sells. I am scared of the consequences of this carelessness, and I cannot be part of it.&lt;/p&gt;

&lt;p&gt;What makes this more urgent is that governments are now actively discussing the use of code that is one big chaos. Legislators and procurement officers are evaluating tools built on codebases where significant portions were generated by AI, reviewed by AI, and merged with minimal human scrutiny. That should demand a higher standard of transparency, not a lower one. Yet many of the people contributing to and shaping these projects operate under pseudonyms. When software influences public infrastructure, healthcare systems, or democratic processes, the public has a right to know whether anyone with real accountability is actually reading the code. Anonymity has its place in open source, but accountability cannot be optional when the stakes are this high.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's Next?
&lt;/h3&gt;

&lt;p&gt;Obvious human carlessness is the first step of losing control. If only AI knows how the code works that drives AI, AI is taking over, maybe in the form of a lobster, or a crab, or digital cancer. OpenClaw is not unique - everybody can vibecode their personal assistant now. Every digital idea can be copied and implemented within no time. The world is about to change drastically. Still, I am not losing hope, instead, I built &lt;a href="https://quotentiroler.github.io/plagiarismchecker" rel="noopener noreferrer"&gt;RepoWatch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This tool points out obvious code quality issues, lists contributors and uses various traditional algorithms to detect duplicate code across PRs. It is derived from an old uni project I made before AI was a thing, where I had to find potential plagiarism in 50 code submissions made by other students. Back then, I did some research and implemented my interpretation of document fingerprinting. I am sure that similar ideas will become more important in the future in order to prevent duplicate data from being processed. My tool aims to bring clearance and transparency so that not only AI can understand code, but also humans, as well as open standards and metrics to determine the uniqueness and consistency of a codebase.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://x.com/steipete" rel="noopener noreferrer"&gt;This is Peter's description on X:&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fecvz9wn2vv4imqw7ulnz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fecvz9wn2vv4imqw7ulnz.png" alt="Peter Steinberger on X" width="800" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's shell that lobster and prevent digital cancer from taking over the world!&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>openclaw</category>
      <category>ai</category>
      <category>openai</category>
    </item>
  </channel>
</rss>
