Build1 publisher2 min readPublished
Putting the Telegram MCP server behind launchd leaves one process holding the session lock
A dev.to harness guide puts numbers on two ways MCP breaks at scale, 8,000 to 15,000 tokens of tool schema on every prompt and a SQLite session lock duplicate stdio processes cannot share. Its fix moves stateful tools onto a supervised loopback daemon.
The Engineer · Build desk

What happened
- A dev.to harness guide reports that MCP setups fall apart past ten tools once stateful services are involved, naming Telegram MTProto sessions, authenticated Chrome, Postgres connection pools and background workers.
- Registering more than 30 tools as MCP JSON Schema consumes 8,000 to 15,000 tokens on every single prompt before the user's request is added, according to the guide.
- Each IDE or subagent restart spawns a duplicate stdio child that tries to take an exclusive lock on the local SQLite session file, crashes with database is locked, and leaves an orphan process.
- The guide's fix rebuilds the stateful Telegram server as a FastMCP daemon speaking SSE and Streamable HTTP on 127.0.0.1 port 8765, holding one warm connection.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- cost At the guide's own numbers, a 30-tool registry bills roughly 267 to 500 tokens per tool, and the context budget pays it again on every turn of the session.
- decision If a CLI wrapper really removes 80% of that overhead, the choice in front of a team stops being which MCP server to add next and becomes which tools deserve a JSON Schema in the prompt at all.
- exposure A warm daemon lends out an authenticated session: anything that can reach the loopback port under your user can call send_broadcast without ever seeing a Telegram credential.
- constraint Adopting the split means owning a boot-time service per stateful tool, with a plist or unit file to maintain, where the stdio config needed only a command and a path.
Telethon keeps its cryptographic session keys in a local SQLite database [7], and SQLite gives the write lock to one process at a time. Under stdio, the agent runtime decides how many processes exist, because the config holds a command and its arguments [5]. Every IDE restart and every subagent launch is another python3 opening the same file, which is where `sqlite3.OperationalError: database is locked` comes from [3]. Pointing the config at a URL instead [9] removes the spawn, and the daemon becomes the only thing that opens `client.session`.
The third failure in the guide is the same lifecycle problem from the other end. Long-running background jobs trigger agent timeouts, and the runtime severs stdin and stdout on the running task [4].
That single ownership depends on the supervisor. The guide registers the daemon as a user-level launchd agent at `com.mika.tg-mcp.plist` so it starts on boot and restarts after a crash [11], and says the result is "100% immune to SQLite locking bugs" [12]. The immunity holds while exactly one instance runs. Leave the old stdio block in a second client's config, or load the plist twice under two labels, and there are two writers on the session file again.
The startup path has a manual step in it. The daemon's lifespan connects the client and raises `RuntimeError("Session not authorized. Run auth helper first.")` when the session is not authorized [10]. Until someone runs that helper by hand, the supervisor is restarting a process that exits immediately.
The token figures come from one author's setup. For 8,000 to 15,000 tokens across 30-plus registered tools [2] to describe your stack, your tool descriptions and parameter schemas have to be about as long as the guide's, and your client has to ship every registered tool definition on every request. The compact CLI wrapper is credited with cutting that overhead by 80% [13], which would leave 1,600 to 3,000 tokens per prompt [15]. The published text breaks off inside the plist, before the wrapper code appears.
I would make the same split on a workstation, and I would draw the line at what a tool holds. Anything holding a socket, a session file, or a connection pool becomes a supervised daemon. Everything read-only stays a process that dies after each answer: a currency converter or a git diff viewer starts, handles one JSON-RPC request over standard I/O, responds, and exits, which the guide calls completely fine [6].
What to watch
- Whether the rest of the guide publishes the CLI wrapper code and a measured before/after token count rather than the 80% figure alone.
- Whether MCP clients add on-demand tool schema loading, which would change the per-tool cost of a 30-tool registry.
- Whether FastMCP's loopback SSE and Streamable HTTP endpoints gain a documented credential story for multi-tenant machines.