{"slug": "stateful-vs-stateless-mcp-why-your-ai-agents-crash-and-how-to-build-a-resilient", "title": "Stateful vs Stateless MCP: Why Your AI Agents Crash and How to Build a Resilient Harness for zsh", "summary": "A developer has published a guide on fixing AI agent crashes caused by stateful Model Context Protocol (MCP) services, arguing that the default stdio process-spawning model breaks stateful tools like Telegram sessions, authenticated browsers, and PostgreSQL pools through database lock contention and orphaned processes. The proposed harness splits tools into stateless stdio utilities and stateful Streamable HTTP daemons run under launchd or systemd, using Python's FastMCP with a lifespan-managed Telegram client, and claims compact CLI wrappers cut token overhead by 80%.", "body_md": "When you connect the first few tools to an AI agent (Cursor, Claude Code, Antigravity, or a custom script) using the Model Context Protocol (MCP), it feels like magic. You define `stdio` servers in your config, and the agent reads files, queries databases, and runs shell commands seamlessly.\n\nHowever, once your setup scales past 10+ tools and includes **stateful services** (Telegram MTProto sessions, authenticated Chrome instances, persistent PostgreSQL connection pools, or background workers), things quickly fall apart:\n\n`sqlite3.OperationalError: database is locked`.` stdin/stdout` pipes.\nIn this guide, we break down how to design a production-grade AI agent harness: separating MCP into **Stateless** and **Stateful** layers, deploying local Streamable HTTP daemons under a system supervisor (`launchd` on macOS or `systemd` on Linux), and slashing token overhead by 80% using compact CLI wrappers.\n\n`stdio` Destroys Stateful Services\nBy default, MCP promotes `stdio` process spawning:\n\n```\n{\n  \"mcpServers\": {\n    \"telegram\": {\n      \"command\": \"python3\",\n      \"args\": [\"/path/to/telegram_server.py\"]\n    }\n  }\n}\n```\n\nFor purely **stateless** utilities (like a currency converter, read-only file viewer, or `git diff`), this is completely fine. The process starts, handles the JSON-RPC request over standard I/O, outputs the response, and exits.\n\nNow, consider what happens with a stateful client like Telegram (via Telethon or Pyrogram):\n\nTelethon stores cryptographic session keys inside a local SQLite database (`anon.session`). When your agent runtime restarts or launches concurrent subagents, a second Python process starts up and tries to access the exact same database. It immediately hits a lock contention, crashes, and leaves a dangling orphan process in your operating system.\n\nTo achieve rock-solid reliability, we split our tool stack into two distinct tiers:\n\n| Metric | Stateless (stdio) | Stateful (Streamable HTTP Daemon) | \n|---|---|---|\n| **Startup Overhead** | Slow (cold-starts Python interpreter every invocation) | Instant (daemon is pre-warmed in memory) | \n| **Socket & Network State** | Dropped on session exit | Maintained 24/7 in background | \n| **Concurrent Access** | Dangerous (risk of file corruptions & locks) | Safe (synchronized via asyncio event loop) | \n| **Resource Footprint** | N processes per N subagents | 1 single lightweight daemon for the entire machine | \n\nLet's convert our stateful service into a long-running daemon supporting Server-Sent Events (SSE) and Streamable HTTP using Python's `FastMCP`:\n\n``` python\n# tg_daemon.py\nimport asyncio\nimport os\nfrom contextlib import asynccontextmanager\nfrom mcp.server.fastmcp import FastMCP\nfrom telethon import TelegramClient\n\nAPI_ID = int(os.environ[\"TG_API_ID\"])\nAPI_HASH = os.environ[\"TG_API_HASH\"]\nSESSION_PATH = os.path.expanduser(\"~/.local/share/tg_session/client.session\")\n\nclient = TelegramClient(SESSION_PATH, API_ID, API_HASH)\n\n@asynccontextmanager\nasync def lifespan(app):\n    await client.connect()\n    if not await client.is_user_authorized():\n        raise RuntimeError(\"Session not authorized. Run auth helper first.\")\n    yield\n    await client.disconnect()\n\nmcp = FastMCP(\"TelegramStatefulService\", lifespan=lifespan)\n\n@mcp.tool()\nasync def send_broadcast(chat_id: str, message: str) -> str:\n    \"\"\"Send message to target chat without reconnecting\"\"\"\n    entity = await client.get_entity(chat_id)\n    sent = await client.send_message(entity, message)\n    return f\"Message delivered successfully. ID: {sent.id}\"\n\n@mcp.tool()\nasync def fetch_unread_summary(limit: int = 10) -> str:\n    \"\"\"Read unread counters from warm connection\"\"\"\n    dialogs = await client.get_dialogs(limit=limit)\n    unreads = [f\"{d.name}: {d.unread_count} unread\" for d in dialogs if d.unread_count > 0]\n    return \"\\n\".join(unreads) if unreads else \"No unread messages\"\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"sse\", host=\"127.0.0.1\", port=8765)\n```\n\nNow the daemon holds a single, warm connection to Telegram servers. It never drops socket connections and is 100% immune to SQLite locking bugs.\n\nIn your agent's MCP configuration, point to the local HTTP endpoint:\n\n```\n{\n  \"mcpServers\": {\n    \"telegram\": {\n      \"url\": \"http://127.0.0.1:8765/sse\"\n    }\n  }\n}\n```\n\n`launchd`\nTo ensure the daemon starts on boot and restarts automatically upon any system crash, register it as a user-level daemon in `~/Library/LaunchAgents/com.mika.tg-mcp.plist`:\n\n```\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>Label</key>\n    <string>com.mika.tg-mcp</string>\n    <key>ProgramArguments</key>\n    <array>\n        <string>/Users/mika/.venv/bin/python3</string>\n        <string>/Users/mika/Project/HarnessSetup/bin/tg_daemon.py</string>\n    </array>\n    <key>RunAtLoad</key>\n    <true/>\n    <key>KeepAlive</key>\n    <dict>\n        <key>SuccessfulExit</key>\n        <false/>\n        <key>NetworkState</key>\n        <true/>\n    </dict>\n    <key>StandardOutPath</key>\n    <string>/Users/mika/.local/share/logs/tg_mcp.log</string>\n    <key>StandardErrorPath</key>\n    <string>/Users/mika/.local/share/logs/tg_mcp_err.log</string>\n</dict>\n</plist>\n```\n\nActivate the service:\n\n```\nlaunchctl load ~/Library/LaunchAgents/com.mika.tg-mcp.plist\n```\n\nIt consumes ~25 MB of RAM, stays active 24/7, and responds instantly.\n\nExposing 35 separate tools with verbose JSON Schema signatures consumes 10,000+ tokens on every single agent iteration.\n\nInstead of registering dozens of raw functions, we provide the agent with a **single unified CLI wrapper**:\n\n``` python\n# tg_cli.py\nimport argparse\nimport httpx\n\nDAEMON_URL = \"http://127.0.0.1:8765\"\n\ndef main():\n    parser = argparse.ArgumentParser(description=\"Compact Telegram CLI for AI Agents\")\n    subparsers = parser.add_subparsers(dest=\"command\")\n\n    send_p = subparsers.add_parser(\"send\", help=\"Send message\")\n    send_p.add_argument(\"--to\", required=True)\n    send_p.add_argument(\"--text\", required=True)\n\n    subparsers.add_parser(\"unreads\", help=\"Fetch unreads\")\n\n    args = parser.parse_args()\n\n    if args.command == \"send\":\n        r = httpx.post(f\"{DAEMON_URL}/tools/send_broadcast\", json={\"chat_id\": args.to, \"message\": args.text})\n        print(r.text)\n    elif args.command == \"unreads\":\n        r = httpx.post(f\"{DAEMON_URL}/tools/fetch_unread_summary\", json={})\n        print(r.text)\n    else:\n        parser.print_help()\n\nif __name__ == \"__main__\":\n    main()\n```\n\n`--help` on demand)`127.0.0.1`.` systemd` to maintain 100% uptime.\n*I regularly share architecture blueprints, production LaunchAgent manifests, and self-hosted AI alternatives in **[OpenSource AI Radar](https://t.me/ossairadar)** and our developer community **[@ossairu](https://t.me/ossairu)**. If you are building autonomous agent infrastructure, feel free to join.*", "url": "https://wpnews.pro/news/stateful-vs-stateless-mcp-why-your-ai-agents-crash-and-how-to-build-a-resilient", "canonical_source": "https://dev.to/m1kulya/stateful-vs-stateless-mcp-why-your-ai-agents-crash-and-how-to-build-a-resilient-harness-for-zsh-3d5k", "published_at": "2026-09-22 10:57:40+00:00", "updated_at": "2026-09-22 11:23:17.200018+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "ai-tools", "developer-tools", "ai-infrastructure"], "entities": ["Model Context Protocol", "Cursor", "Claude Code", "Antigravity", "Telegram", "Telethon", "Pyrogram", "FastMCP"], "alternates": {"html": "https://wpnews.pro/news/stateful-vs-stateless-mcp-why-your-ai-agents-crash-and-how-to-build-a-resilient", "markdown": "https://wpnews.pro/news/stateful-vs-stateless-mcp-why-your-ai-agents-crash-and-how-to-build-a-resilient.md", "text": "https://wpnews.pro/news/stateful-vs-stateless-mcp-why-your-ai-agents-crash-and-how-to-build-a-resilient.txt", "jsonld": "https://wpnews.pro/news/stateful-vs-stateless-mcp-why-your-ai-agents-crash-and-how-to-build-a-resilient.jsonld"}}