



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/xendpoint → 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
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_jobis 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_trending → download_video → extract_viral_clips → tool_captions → bulk_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)
hi there, give it a try and let me know you opinions. support@viralmint.net