DEV Community

Cover image for I put an MCP server inside a desktop app so Claude can make videos on my machine
ViralMint
ViralMint

Posted on

I put an MCP server inside a desktop app so Claude can make videos on my machine





 I've been building ViralMint — 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.

A few months in, I realized the most interesting feature wasn't any single tool — it was making the whole app 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.

This post covers the architecture and the three gotchas that cost me the most time.

Why MCP-in-the-app instead of a separate server

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 on it (fastmcp, HTTP Streamable transport, /mcp route). One process, one port, ships with the app.

The tools don't import my services directly. They call the app's own REST API over loopback:

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

The cost is ~1ms of TCP loopback per call. Fine.

Gotcha 1: app.mount() doesn't start your sub-app

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

Fix: enter the sub-app's lifespan from the parent's lifespan:

@asynccontextmanager
async def lifespan(app: FastAPI):
    async with AsyncExitStack() as stack:
        await stack.enter_async_context(
            mcp_asgi.router.lifespan_context(mcp_asgi)
        )
        yield
Enter fullscreen mode Exit fullscreen mode

Gotcha 2: the /mcp/mcp path trap

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

Gotcha 3: CSRF middleware vs. non-browser clients

My CSRF middleware rejects cross-origin browser requests. MCP clients send no Origin/Referer 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, 0600 on disk, constant-time compare) plus binding to 127.0.0.1.

If your CSRF check blocks missing-Origin requests, you'll need that exception — and a token like this to make it safe.

Design choices that made agents actually good at it

  • Macros before primitives. Tools register with workflow shortcuts first (analyze_competitor, find_breakouts); LLMs reach for whatever they see first, so make the first thing the 1-call version of a 7-call chain.
  • wait_for_job is the single most important tool. Everything long-running returns {job_id} 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.
  • Structured errors, not tracebacks. Every failure returns an envelope with a hint (402 → "credit exhausted", "chromium_not_installed" + a settings deep-link). Agents recover well when errors say what to do next.
  • Descriptions are for the LLM. One sentence of purpose, every arg with constraints, return shape at the end — so the model can chain tools without guessing.

What it looks like in practice

Me, in Claude Code: "find what's breaking out in the woodworking niche, grab the top video, extract 3 clips, caption them, give me the files." Claude calls scout_trendingdownload_videoextract_viral_clipstool_captionsbulk_export_videos, 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.

Code

The pipeline is open source (AGPL-3.0): https://github.com/openclaw-easy/ViralMint — desktop app with docs at https://viralmint.net. The MCP patterns above (loopback-to-self, macros-first registration, the job poller) transfer to any local app you want to make agent-drivable.

Happy to answer questions about fastmcp mounting, the tool design, or the video pipeline itself.

Top comments (1)

Collapse
 
xinru_tang_3683bf5fbfdd6a profile image
ViralMint • Edited

hi there, give it a try and let me know you opinions. support@viralmint.net