<?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: ViralMint</title>
    <description>The latest articles on DEV Community by ViralMint (@xinru_tang_3683bf5fbfdd6a).</description>
    <link>https://dev.to/xinru_tang_3683bf5fbfdd6a</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%2F3951142%2F106e5175-5c00-450e-880f-ecbb11247e35.png</url>
      <title>DEV Community: ViralMint</title>
      <link>https://dev.to/xinru_tang_3683bf5fbfdd6a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xinru_tang_3683bf5fbfdd6a"/>
    <language>en</language>
    <item>
      <title>I put an MCP server inside a desktop app so Claude can make videos on my machine</title>
      <dc:creator>ViralMint</dc:creator>
      <pubDate>Fri, 17 Jul 2026 13:53:11 +0000</pubDate>
      <link>https://dev.to/xinru_tang_3683bf5fbfdd6a/i-put-an-mcp-server-inside-a-desktop-app-so-claude-can-make-videos-on-my-machine-1g2f</link>
      <guid>https://dev.to/xinru_tang_3683bf5fbfdd6a/i-put-an-mcp-server-inside-a-desktop-app-so-claude-can-make-videos-on-my-machine-1g2f</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3y5xkh7snjd15taj169v.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3y5xkh7snjd15taj169v.png" alt=" " width="800" height="502"&gt;&lt;/a&gt;&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc0qnp3vpbt9vmaktkfvt.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc0qnp3vpbt9vmaktkfvt.png" alt=" " width="800" height="502"&gt;&lt;/a&gt;&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp80efdb03um53wgd46wy.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp80efdb03um53wgd46wy.png" alt=" " width="800" height="502"&gt;&lt;/a&gt;&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl39l8bxvwcjs3d3roj4l.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl39l8bxvwcjs3d3roj4l.png" alt=" " width="800" height="502"&gt;&lt;/a&gt;&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F40fozd57befdrgxnw2bg.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F40fozd57befdrgxnw2bg.png" alt=" " width="800" height="502"&gt;&lt;/a&gt;I've been building &lt;strong&gt;ViralMint&lt;/strong&gt; — an open-source desktop app that scouts trending videos, clips long videos into shorts, and generates AI videos locally. FastAPI backend, React frontend, the heavy lifting (Whisper transcription, yt-dlp, FFmpeg) all runs on your machine.&lt;/p&gt;

&lt;p&gt;A few months in, I realized the most interesting feature wasn't any single tool — it was making the &lt;em&gt;whole app&lt;/em&gt; drivable by an AI agent. So I mounted an MCP (Model Context Protocol) server inside it. Now Claude Code / Cursor can scout a niche, download a competitor's video, extract the viral clips, caption them, and export platform-ready mp4s — by calling my app's tools directly on your machine.&lt;/p&gt;

&lt;p&gt;This post covers the architecture and the three gotchas that cost me the most time.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why MCP-in-the-app instead of a separate server
&lt;/h2&gt;

&lt;p&gt;The usual MCP setup is a standalone process you configure per client. But my backend is already a local FastAPI server — so I mounted the MCP server &lt;em&gt;on it&lt;/em&gt; (fastmcp, HTTP Streamable transport, &lt;code&gt;/mcp&lt;/code&gt; route). One process, one port, ships with the app.&lt;/p&gt;

&lt;p&gt;The tools don't import my services directly. They call the app's own REST API over loopback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stable contract.&lt;/strong&gt; Internal service signatures change; the public API doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free middleware.&lt;/strong&gt; Auth, rate limits, and every future request guard apply to MCP calls automatically — the agent can't accidentally bypass a check the UI enforces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheap to extend.&lt;/strong&gt; New &lt;code&gt;/api/x&lt;/code&gt; endpoint → thin &lt;code&gt;@mcp.tool()&lt;/code&gt; wrapper. That's the whole recipe, 102 tools later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost is ~1ms of TCP loopback per call. Fine.&lt;/p&gt;
&lt;h2&gt;
  
  
  Gotcha 1: &lt;code&gt;app.mount()&lt;/code&gt; doesn't start your sub-app
&lt;/h2&gt;

&lt;p&gt;FastAPI's &lt;code&gt;mount()&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; forward lifespan startup to a mounted ASGI app. The MCP session manager never starts, and every &lt;code&gt;initialize&lt;/code&gt; fails with a cryptic &lt;code&gt;Session terminated&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fix: enter the sub-app's lifespan from the parent's lifespan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@asynccontextmanager&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lifespan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;AsyncExitStack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enter_async_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;mcp_asgi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lifespan_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcp_asgi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gotcha 2: the &lt;code&gt;/mcp/mcp&lt;/code&gt; path trap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;mcp.http_app()&lt;/code&gt; defaults its internal route to &lt;code&gt;/mcp&lt;/code&gt;. Mount that at &lt;code&gt;/mcp&lt;/code&gt; and your endpoint is &lt;code&gt;/mcp/mcp&lt;/code&gt;. Pass &lt;code&gt;path="/"&lt;/code&gt; to &lt;code&gt;http_app()&lt;/code&gt; so the route sits at the sub-app root, then mount at &lt;code&gt;/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 3: CSRF middleware vs. non-browser clients
&lt;/h2&gt;

&lt;p&gt;My CSRF middleware rejects cross-origin browser requests. MCP clients send no &lt;code&gt;Origin&lt;/code&gt;/&lt;code&gt;Referer&lt;/code&gt; at all — which my middleware already treated as "non-browser client, allow." So MCP flowed through with zero special-casing. The real boundary is a bearer token (generated on first run, &lt;code&gt;0600&lt;/code&gt; on disk, constant-time compare) plus binding to &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If your CSRF check &lt;em&gt;blocks&lt;/em&gt; missing-Origin requests, you'll need that exception — and a token like this to make it safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design choices that made agents actually good at it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Macros before primitives.&lt;/strong&gt; Tools register with workflow shortcuts first (&lt;code&gt;analyze_competitor&lt;/code&gt;, &lt;code&gt;find_breakouts&lt;/code&gt;); LLMs reach for whatever they see first, so make the first thing the 1-call version of a 7-call chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;wait_for_job&lt;/code&gt; is the single most important tool.&lt;/strong&gt; Everything long-running returns &lt;code&gt;{job_id}&lt;/code&gt; immediately; one poller tool turns the whole surface into blocking calls an agent can reason about. It also returns the output file path, so the agent doesn't need a download step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured errors, not tracebacks.&lt;/strong&gt; Every failure returns an envelope with a hint (&lt;code&gt;402 → "credit exhausted"&lt;/code&gt;, &lt;code&gt;"chromium_not_installed"&lt;/code&gt; + a settings deep-link). Agents recover well when errors say what to do next.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descriptions are for the LLM.&lt;/strong&gt; One sentence of purpose, every arg with constraints, return shape at the end — so the model can chain tools without guessing.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Me, in Claude Code: &lt;em&gt;"find what's breaking out in the woodworking niche, grab the top video, extract 3 clips, caption them, give me the files."&lt;/em&gt; Claude calls &lt;code&gt;scout_trending&lt;/code&gt; → &lt;code&gt;download_video&lt;/code&gt; → &lt;code&gt;extract_viral_clips&lt;/code&gt; → &lt;code&gt;tool_captions&lt;/code&gt; → &lt;code&gt;bulk_export_videos&lt;/code&gt;, and the mp4s land in a folder. The app's UI shows the agent's activity live over a WebSocket, so you can watch it work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The pipeline is open source (AGPL-3.0): &lt;strong&gt;&lt;a href="https://github.com/openclaw-easy/ViralMint" rel="noopener noreferrer"&gt;https://github.com/openclaw-easy/ViralMint&lt;/a&gt;&lt;/strong&gt; — desktop app with docs at &lt;a href="https://viralmint.net" rel="noopener noreferrer"&gt;https://viralmint.net&lt;/a&gt;. The MCP patterns above (loopback-to-self, macros-first registration, the job poller) transfer to any local app you want to make agent-drivable.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about fastmcp mounting, the tool design, or the video pipeline itself.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Drive Video Production From Claude Code: ViralMint's MCP Server Explained</title>
      <dc:creator>ViralMint</dc:creator>
      <pubDate>Mon, 25 May 2026 17:41:19 +0000</pubDate>
      <link>https://dev.to/xinru_tang_3683bf5fbfdd6a/drive-video-production-from-claude-code-viralmints-mcp-server-explained-4hm</link>
      <guid>https://dev.to/xinru_tang_3683bf5fbfdd6a/drive-video-production-from-claude-code-viralmints-mcp-server-explained-4hm</guid>
      <description></description>
      <category>claudecode</category>
      <category>mcp</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
