<?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: Mayur Rawte</title>
    <description>The latest articles on DEV Community by Mayur Rawte (@mayurrawte).</description>
    <link>https://dev.to/mayurrawte</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%2F4019646%2F8a326910-aab3-4482-8eda-f0f75c63df49.jpg</url>
      <title>DEV Community: Mayur Rawte</title>
      <link>https://dev.to/mayurrawte</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mayurrawte"/>
    <language>en</language>
    <item>
      <title>Render MCP Apps in your own host in 5 minutes</title>
      <dc:creator>Mayur Rawte</dc:creator>
      <pubDate>Tue, 01 Sep 2026 17:18:49 +0000</pubDate>
      <link>https://dev.to/mayurrawte/render-mcp-apps-in-your-own-host-in-5-minutes-4bli</link>
      <guid>https://dev.to/mayurrawte/render-mcp-apps-in-your-own-host-in-5-minutes-4bli</guid>
      <description>&lt;p&gt;In January, &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-01-26-mcp-apps/" rel="noopener noreferrer"&gt;MCP Apps (SEP-1865)&lt;/a&gt; became the first official extension to the Model Context Protocol, co-authored by Anthropic and OpenAI. Tools can now return interactive UIs — dashboards, forms, viewers — and Claude, ChatGPT, Goose, and VS Code render them inline in the conversation.&lt;/p&gt;

&lt;p&gt;That's great if your users live in Claude. But a lot of us are building our &lt;em&gt;own&lt;/em&gt; hosts: internal agent consoles, MCP inspectors, vertical chat products, gateway UIs. And the moment a connected server declares a &lt;code&gt;ui://&lt;/code&gt; resource, you're on the hook for the host side of the spec:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fetch the resource and render its HTML in a &lt;strong&gt;sandboxed iframe&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;construct and enforce the &lt;strong&gt;CSP&lt;/strong&gt; the resource declared in &lt;code&gt;_meta.ui.csp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;answer the app's &lt;strong&gt;&lt;code&gt;ui/initialize&lt;/code&gt;&lt;/strong&gt; request over JSON-RPC/postMessage, and hold all notifications until it confirms &lt;code&gt;ui/notifications/initialized&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;deliver &lt;strong&gt;&lt;code&gt;ui/notifications/tool-input&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;tool-result&lt;/code&gt;&lt;/strong&gt; at the right lifecycle moments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;proxy &lt;code&gt;tools/call&lt;/code&gt;&lt;/strong&gt; from the app back to your MCP client (respecting tool visibility)&lt;/li&gt;
&lt;li&gt;resize the iframe on &lt;strong&gt;&lt;code&gt;ui/notifications/size-changed&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;ask the app to wind down with &lt;strong&gt;&lt;code&gt;ui/resource-teardown&lt;/code&gt;&lt;/strong&gt; before you unmount it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of it is hard. All of it is fiddly, and every host rebuilds it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5-minute version
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;McpAppFrame&lt;/code&gt; from &lt;a href="https://mcp-elements.wearesnx.studio" rel="noopener noreferrer"&gt;mcp-elements&lt;/a&gt; is a copy-paste React component (shadcn model — the source lands in your repo) that implements the host side of SEP-1865:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx mcp-elements add mcp-app-frame
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;McpAppFrame&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;@/components/mcp-app-frame&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Your MCP client reads the ui:// resource the tool declares&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contents&lt;/span&gt; &lt;span class="p"&gt;}&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readResource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resourceUri&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;resource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;// mimeType: text/html;profile=mcp-app&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Render it&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;McpAppFrame&lt;/span&gt;
  &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;resourceMeta&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_meta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;hostContext&lt;/span&gt;&lt;span class="o"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="nx"&gt;callTool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;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="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arguments&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="nx"&gt;toolInput&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;toolArgs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;toolResult&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;openLink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_blank&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;noopener&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a working SEP-1865 host surface. The frame:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;renders the HTML via &lt;code&gt;srcdoc&lt;/code&gt; with &lt;code&gt;sandbox="allow-scripts"&lt;/code&gt; (no &lt;code&gt;allow-same-origin&lt;/code&gt; — the app gets an opaque origin),&lt;/li&gt;
&lt;li&gt;injects a &lt;code&gt;&amp;lt;meta http-equiv="Content-Security-Policy"&amp;gt;&lt;/code&gt; built from the resource's declared &lt;code&gt;connectDomains&lt;/code&gt; / &lt;code&gt;resourceDomains&lt;/code&gt; / &lt;code&gt;frameDomains&lt;/code&gt;, falling back to the spec's restrictive default (no external access) when the metadata is absent,&lt;/li&gt;
&lt;li&gt;answers &lt;code&gt;ui/initialize&lt;/code&gt; with your &lt;code&gt;hostInfo&lt;/code&gt;, capabilities derived from which delegates you passed, and &lt;code&gt;hostContext&lt;/code&gt; (theme, container dimensions, locale),&lt;/li&gt;
&lt;li&gt;queues &lt;code&gt;tool-input&lt;/code&gt; / &lt;code&gt;tool-result&lt;/code&gt; notifications until the app sends &lt;code&gt;ui/notifications/initialized&lt;/code&gt; — the spec forbids sending anything earlier,&lt;/li&gt;
&lt;li&gt;proxies the app's &lt;code&gt;tools/call&lt;/code&gt; and &lt;code&gt;resources/read&lt;/code&gt; requests to the delegates you provided, returning JSON-RPC errors when you didn't,&lt;/li&gt;
&lt;li&gt;grows the iframe as the app reports &lt;code&gt;size-changed&lt;/code&gt;, and&lt;/li&gt;
&lt;li&gt;sends &lt;code&gt;ui/resource-teardown&lt;/code&gt; on unmount and gives the app a moment to respond.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's runtime-free — there is no MCP client inside. You bring &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt;, &lt;code&gt;mcp-use&lt;/code&gt;, or your own; the component only asks for two functions.&lt;/p&gt;

&lt;p&gt;If you'd rather own the protocol logic without React, the same state machine is a plain-TypeScript factory in &lt;code&gt;@mcp-elements/core&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;createAppHost&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;@mcp-elements/core&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;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createAppHost&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;iframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;callTool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;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="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arguments&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;iframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentWindow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What about @mcp-ui/client and the official SDK?
&lt;/h2&gt;

&lt;p&gt;Use them! &lt;a href="https://mcpui.dev/" rel="noopener noreferrer"&gt;&lt;code&gt;@mcp-ui/client&lt;/code&gt;&lt;/a&gt; and the &lt;code&gt;ext-apps&lt;/code&gt; App Bridge are excellent, and they're npm dependencies maintained by the community and the MCP org. mcp-elements takes the shadcn stance instead: the source is copied into your repo, so when you need to change how consent looks or where tool results render, you edit a file you own. It also comes with the rest of the host surface — the OAuth consent dialog, scope inspector, tool-call card, resource browser — which no SDK ships.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Live demo (a real app doing the handshake in the frame): &lt;a href="https://mcp-elements.wearesnx.studio/components/mcp-app-frame" rel="noopener noreferrer"&gt;https://mcp-elements.wearesnx.studio/components/mcp-app-frame&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo (MIT): &lt;a href="https://github.com/mcp-elements/ui" rel="noopener noreferrer"&gt;https://github.com/mcp-elements/ui&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The full host kit: &lt;a href="https://mcp-elements.wearesnx.studio" rel="noopener noreferrer"&gt;https://mcp-elements.wearesnx.studio&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm building this in the open — if you're building an MCP host and something's missing from the set, open an issue and tell me what screen you had to hand-roll.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>software</category>
    </item>
    <item>
      <title>Every MCP client rebuilds the same seven screens</title>
      <dc:creator>Mayur Rawte</dc:creator>
      <pubDate>Tue, 07 Jul 2026 13:51:16 +0000</pubDate>
      <link>https://dev.to/mayurrawte/every-mcp-client-rebuilds-the-same-seven-screens-1764</link>
      <guid>https://dev.to/mayurrawte/every-mcp-client-rebuilds-the-same-seven-screens-1764</guid>
      <description>&lt;p&gt;MCP went from 2M to 97M monthly SDK downloads in a year. There are over 9,400 servers in the registry. A lot of people are suddenly building MCP clients — and if you've built one, you already know where this is going, because you've built these screens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A card that shows a tool call: name, arguments, a spinner, then a result or an error with a retry button.&lt;/li&gt;
&lt;li&gt;A consent dialog that lists OAuth scopes in language a human can approve.&lt;/li&gt;
&lt;li&gt;A form generated from a tool's JSON Schema, with validation.&lt;/li&gt;
&lt;li&gt;A scope inspector for "what exactly can this server touch?"&lt;/li&gt;
&lt;li&gt;A resource browser for whatever the server exposes.&lt;/li&gt;
&lt;li&gt;A connection status badge.&lt;/li&gt;
&lt;li&gt;And if you're on the MCP Apps spec: a sandboxed iframe with a postMessage bridge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are hard the first time you describe them. All of them are annoying the third time you build them. The tool-call card alone is a state machine with six states (idle, pending, running, done, error, cancelled), retry logic, theme tokens, and accessibility work — call it a week if you do it properly, which is exactly why most clients don't.&lt;/p&gt;

&lt;p&gt;The chat side of this stack is well served. Vercel's AI Elements, assistant-ui, CopilotKit — good projects. But they're React-only (CopilotKit also does Angular), and none of them ship the MCP-specific pieces. The one multi-framework option, Deep Chat, is a single monolithic widget, not primitives you can compose. When I went looking for a consent dialog I could copy into an Angular app, there wasn't one. So I built the set.&lt;/p&gt;

&lt;h2&gt;
  
  
  What mcp-elements is
&lt;/h2&gt;

&lt;p&gt;38 components you copy into your project with a CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx mcp-elements add mcp-tool-call
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That drops the component source, its CSS, and the core state machine it uses into your repo, with imports rewritten to relative paths. There is no runtime dependency to upgrade or get broken by. If you want to change how the retry button works, you edit the file. It's the shadcn model, applied to MCP.&lt;/p&gt;

&lt;p&gt;Seven components are MCP-specific — the list above. The other 31 are the base and AI pieces you need around them: chat bubble, streaming text, prompt input, source card, buttons, dialogs, the usual.&lt;/p&gt;

&lt;p&gt;Here's a full MCP flow in three components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;McpServerStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;McpToolCall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;McpConsentDialog&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;@mcp-elements/react&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;createToolState&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;@mcp-elements/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Connection state&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;McpServerStatus&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;connected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;serverName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github-mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// 2. OAuth consent&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;McpConsentDialog&lt;/span&gt;
  &lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showConsent&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;serverName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"GitHub MCP"&lt;/span&gt;
  &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;repo:read&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.email:read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onApprove&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleApprove&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onDeny&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleDeny&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Tool execution&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createToolState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;toolState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search_repos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&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;mcp-elements&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;McpToolCall&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;toolState&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;onRetry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;toolState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why it works in React, Angular, and Vue
&lt;/h2&gt;

&lt;p&gt;The architecture is three layers, and the layering is the whole trick:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CSS&lt;/strong&gt; — plain stylesheets with OKLCH design tokens. No CSS-in-JS, no build coupling. Override the tokens, get a theme.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Core&lt;/strong&gt; — plain TypeScript state machines: tool lifecycle, consent flow, JSON-Schema-to-form. No framework imports anywhere in the package.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adapters&lt;/strong&gt; — thin React, Angular, and Vue components that subscribe to the core machines and apply the CSS classes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because the state machine and the stylesheet are shared, the React and Angular versions of &lt;code&gt;McpToolCall&lt;/code&gt; don't just look the same — they &lt;em&gt;behave&lt;/em&gt; the same, transition for transition. When I fixed a focus-management bug in the dialog last week, the fix was the same fix in all three frameworks, because the bug lived in one place.&lt;/p&gt;

&lt;p&gt;This is also my answer to a complaint I kept seeing in issue trackers: the Svelte request on vercel/ai-elements has been open since September 2025. Framework-agnostic core plus thin adapters means adding a framework is a bounded amount of work, not a rewrite. Svelte and Lit adapters are on the roadmap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unglamorous parts
&lt;/h2&gt;

&lt;p&gt;Things that took longer than the happy path, and that you get for free by copying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Focus management.&lt;/strong&gt; The dialog moves focus into itself on open, traps Tab, closes on Escape, and restores focus to the trigger on close. Most hand-rolled dialogs get at least one of those wrong; mine did too, for a while.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen readers during streaming.&lt;/strong&gt; Connection status uses &lt;code&gt;aria-live&lt;/code&gt; so state changes are announced. Almost no AI component library handles this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contrast.&lt;/strong&gt; Both themes pass WCAG AA. The dark theme's muted text is 9.4:1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The six states.&lt;/strong&gt; Tool calls fail, get cancelled, and get retried. The card renders all of it, not just the demo path.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it isn't
&lt;/h2&gt;

&lt;p&gt;It's not an MCP client SDK — you bring your own connection and wire its events into the state machines. It's not a chat framework; there's no runtime, no provider, no cloud anything. And it's v0.1: all seven MCP components ship for all three frameworks, the base set is complete in React and Angular, and Vue has ten of the base components so far, with the rest in progress.&lt;/p&gt;

&lt;p&gt;The MCP Apps frame tracks the spec as published in January 2026. The spec is young. If it moves, the component moves — and since you own the copy in your repo, you decide when to take that update.&lt;/p&gt;

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

&lt;p&gt;The docs site renders every component live on the page — the playground demos are the actual components running, not screenshots:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docs and playground: &lt;a href="https://mcp-elements.wearesnx.studio" rel="noopener noreferrer"&gt;https://mcp-elements.wearesnx.studio&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/mcp-elements/ui" rel="noopener noreferrer"&gt;https://github.com/mcp-elements/ui&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npx mcp-elements add mcp-tool-call&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MIT licensed. If you've built an MCP client and something you needed isn't in the set, open an issue — the next batch of components gets picked from real gaps, not my guesses.&lt;/p&gt;

</description>
      <category>react</category>
      <category>opensource</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
