<?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: Shaxzod Ahmedov</title>
    <description>The latest articles on DEV Community by Shaxzod Ahmedov (@shaxzod_ahmedov_f81d92240).</description>
    <link>https://dev.to/shaxzod_ahmedov_f81d92240</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%2F1625783%2F182eae1d-eadb-4349-a2b1-e282c2f73117.jpg</url>
      <title>DEV Community: Shaxzod Ahmedov</title>
      <link>https://dev.to/shaxzod_ahmedov_f81d92240</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shaxzod_ahmedov_f81d92240"/>
    <language>en</language>
    <item>
      <title>Control your ESP32 from an AI agent: MCP + a few lines of C++</title>
      <dc:creator>Shaxzod Ahmedov</dc:creator>
      <pubDate>Sun, 28 Jun 2026 13:09:41 +0000</pubDate>
      <link>https://dev.to/shaxzod_ahmedov_f81d92240/control-your-esp32-from-an-ai-agent-mcp-a-few-lines-of-c-2o87</link>
      <guid>https://dev.to/shaxzod_ahmedov_f81d92240/control-your-esp32-from-an-ai-agent-mcp-a-few-lines-of-c-2o87</guid>
      <description>&lt;p&gt;Language models are finally good enough to &lt;em&gt;act&lt;/em&gt;, not just chat — but giving one hands on real&lt;br&gt;
hardware is still more plumbing than it should be. In this post I'll wire an &lt;strong&gt;ESP32&lt;/strong&gt; to an AI&lt;br&gt;
agent using the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;, where every dashboard widget I declare in C++&lt;br&gt;
becomes a tool the agent can call: read a temperature, flip a relay, set a threshold. The same&lt;br&gt;
few lines also give me a real-time web dashboard for humans.&lt;/p&gt;
&lt;h2&gt;
  
  
  MCP in one paragraph
&lt;/h2&gt;

&lt;p&gt;MCP is a small open protocol that lets an AI client (Claude Desktop, Claude Code, Cursor…)&lt;br&gt;
discover and call &lt;strong&gt;tools&lt;/strong&gt; exposed by a server. A tool is just a named function with a schema —&lt;br&gt;
&lt;code&gt;get_temperature&lt;/code&gt;, &lt;code&gt;set_pump&lt;/code&gt;, and so on. The agent lists the tools, then calls them. That's it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The library: declare widgets, get tools for free
&lt;/h2&gt;

&lt;p&gt;I use &lt;a href="https://github.com/ziyarago/RisalDash" rel="noopener noreferrer"&gt;&lt;strong&gt;RisalDash&lt;/strong&gt;&lt;/a&gt; — an ESP32/ESP8266 library where you&lt;br&gt;
declare widgets in C++ and it generates the whole web UI &lt;strong&gt;and&lt;/strong&gt; the protocol. The same widget&lt;br&gt;
declaration that powers the human dashboard also defines the agent's tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;cpp&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;RisalUI.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;span class="n"&gt;RisalUI&lt;/span&gt; &lt;span class="nf"&gt;dash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Greenhouse"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;24.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;pump&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;dash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gauge&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Temperature"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;temp&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="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"C"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;dash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Pump"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pump&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[](&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt; &lt;span class="n"&gt;digitalWrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PUMP_PIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="n"&gt;dash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Target"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="n"&gt;dash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enableMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a_secret_token"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;-- this line turns the widgets into MCP tools&lt;/span&gt;
  &lt;span class="n"&gt;dash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;dash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&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;enableMCP()&lt;/code&gt; exposes &lt;code&gt;GET /api/mcp/manifest&lt;/code&gt; on the device: every widget becomes a &lt;code&gt;get_*&lt;/code&gt;&lt;br&gt;
(read) and/or &lt;code&gt;set_*&lt;/code&gt; (write) tool.&lt;/p&gt;
&lt;h2&gt;
  
  
  Connect an agent
&lt;/h2&gt;

&lt;p&gt;The device speaks its own little manifest; a tiny bridge turns that into a real MCP server over&lt;br&gt;
stdio. It's published on npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RISAL_ESP_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://192.168.1.42 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;RISAL_MCP_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;a_secret_token &lt;span class="se"&gt;\&lt;/span&gt;
npx risal-dash-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Claude Desktop's config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"greenhouse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"risal-dash-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"RISAL_ESP_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://192.168.1.42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"RISAL_MCP_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a_secret_token"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"What's the greenhouse temperature, and turn the pump on if it's above 26°C."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent calls &lt;code&gt;get_temperature&lt;/code&gt;, reasons, and calls &lt;code&gt;set_pump&lt;/code&gt; — on your actual ESP32.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it stays tiny and offline
&lt;/h2&gt;

&lt;p&gt;A nice side effect: the dashboard the device serves is &lt;strong&gt;offline-first&lt;/strong&gt;. On first boot it raises&lt;br&gt;
a Wi-Fi access point with a captive portal, you pick your network, and from then on it serves a&lt;br&gt;
real-time UI from flash — system fonts, no CDN, no external requests. Values stream over WebSocket;&lt;br&gt;
controls call back into your code.&lt;/p&gt;

&lt;p&gt;It's also &lt;strong&gt;Zero-Waste&lt;/strong&gt;: the linker (&lt;code&gt;--gc-sections&lt;/code&gt;) strips widget types you don't use, so a&lt;br&gt;
type you don't call costs &lt;strong&gt;0 bytes&lt;/strong&gt;, and one you do adds ~1.3–3.4 KB (its C++ + CSS + JS).&lt;br&gt;
Builds for both ESP32 and ESP8266.&lt;/p&gt;

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

&lt;p&gt;There's a live, code-to-dashboard demo you can poke in the browser (no board needed):&lt;br&gt;
&lt;strong&gt;&lt;a href="https://dash.risal.io/showcase" rel="noopener noreferrer"&gt;https://dash.risal.io/showcase&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Library (MIT): &lt;a href="https://github.com/ziyarago/RisalDash" rel="noopener noreferrer"&gt;https://github.com/ziyarago/RisalDash&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MCP bridge: &lt;a href="https://github.com/ziyarago/risal-dash-mcp" rel="noopener noreferrer"&gt;https://github.com/ziyarago/risal-dash-mcp&lt;/a&gt; (&lt;code&gt;npx risal-dash-mcp&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build something with it — or have ideas for the API or which device "tools" an LLM should&lt;br&gt;
get — I'd love to hear it.&lt;/p&gt;

</description>
      <category>esp32</category>
      <category>esp8266</category>
      <category>iot</category>
      <category>cpp</category>
    </item>
  </channel>
</rss>
