Hook
The Windows agent went silent mid-session. It had been routing tasks through ark-delegator — a cross-environment bridge that lives in WSL. But the MCP server behind it had crashed with OpenCode's last daemon restart, because it was registered as a plugin. Same process. Same failure. Same data loss.
I checked the process table. ark-memory was still running on port 8102. ark-exec was healthy on 8101. Every MCP server was alive — because they were managed by systemd, not by OpenCode. The plugin was dead because plugins live inside the agent's process. The MCP servers were fine because MCP servers are independent.
That was the day I stopped treating MCP servers as plugins.
The Stack
oh-my-mcp (systemd user service)
├── port 8080: Management API
├── port 8090: Gateway (MCP proxy)
└── child processes (supervised):
├── 8101 ark-exec → exec MCP tools
├── 8102 ark-memory → memory knowledge graph
├── 8103 ark-resolve → URI resolution
├── 8104 mempalace → vector semantic search
├── 8105 ark-gist → GitHub Gist operations
└── 8106 ark-delegator → cross-agent task routing
server-commands-rtk (the CLI bridge)
├── stdin/stdout → spawns processes
├── RTK filter → ~90% token reduction on large output
└── audit logger → append-only JSONL with rotation
Six MCP servers, each with 3-8 focused tools. One CLI bridge for everything else. Total schema overhead: ~8,000-10,000 tokens across all six — less than a single GitHub MCP server alone.
Every server is a standalone process. Every one can restart independently. Every one is reachable from any client on the network via SSE on its port. The plugin model gives you in-process speed. The MCP server model gives you survival.
The Architecture Decision
# oh-my-mcp config.yaml
servers:
ark-exec: transport: stdio
ark-memory: transport: stdio
ark-resolve: transport: stdio
mempalace: transport: stdio
ark-gist: transport: stdio
ark-delegator: transport: stdio
All MCP. All focused. No monolithic 93-tool server. The decision was deliberate: six narrow servers mean the agent loads only the schemas it needs for the current operation, not a catalog of everything it might ever do.
For raw commands, we route through server-commands-rtk:
server-commands-rtk_run_process({
command: "npm view @ev3lynx/md-analyzer",
cwd: "/home/ev3lynx",
description: "check latest version"
})
// Returns: { stdout, stderr, exitCode, duration_ms, error_type }
This is the hybrid in practice. Structured operations through MCP (memory, search, CRUD). Everything else through a CLI wrapper that happens to look like an MCP tool.
The Numbers
Package downloads check — a real trace from running both paths through our commands-rtk CLI bridge:
npmPackageDownloads("md-analyzer")
→ 170 tokens, 3 fields
npm view @ev3lynx/md-analyzer | grep -A5 downloads
→ 2,000+ tokens unfiltered, ~400 with RTK
MCP tool wins: 2.4x cheaper than raw+RTK, 12x cheaper than unfiltered.
Over a thousand such checks per week — that token difference adds up. Not enough to matter on one call. Enough to matter across a team's collective session hours at $3-15/M tokens depending on your model tier.
Git push:
git push origin main
→ ~50 tokens command + ~50 tokens output
Raw CLI wins: zero schema overhead, same result.
The rule: if the output has a known schema, build a tool. If the command is an action with small output, run it raw.
The audit logger
server-commands-rtk logs every command:
{
"timestamp": "2026-07-08T10:01:27Z",
"command": "git push origin main",
"exitCode": 0,
"duration_ms": 3240,
"rtk_filtered": true
}
This is the insight that MCP-vs-CLI debates miss: audit trails don't require MCP. A CLI wrapper with structured logging gives you the same compliance surface as MCP's structured I/O. The difference is architectural — logging at the bridge layer, not the protocol layer.
The Conflict
The next morning I ran systemctl --user status oh-my-mcp. Every service was green. ark-exec, ark-memory, ark-gist — all running, all within their restart budgets. The plugin had died. The MCP servers had not.
It's not MCP vs CLI. It's single-process vs independent-process.
The failing plugin shared OpenCode's process table. When the daemon restarted, the plugin restarted with it — but it surfaced with empty connection pools, a cold cache, and every in-flight task evaporated. The MCP server on port 8106 had never noticed the restart. Its client had disconnected. It kept receiving. It was still there when the next session connected.
Lifecycle independence. A plugin inherits its parent's mortality. An MCP server manages its own.
This sounds architectural-theoretical until you run a 24-hour agent managing stateful connections across six backends. Then it reveals itself as the difference between a system that recovers its own state and one that leaks it, session by session.
The Resolution
The pattern we settled on is three-tier:
MCP server (focused, 3-8 tools)
→ structured operations the agent uses every session
→ independent lifecycle, restart survives client death
CLI bridge (server-commands-rtk with RTK + audit)
→ ad-hoc commands, small output, side effects
→ audit logging without MCP schema overhead
Hybrid dispatch (decision at the agent level)
→ Query/search/read? MCP tool
→ Execute/write/push? Raw CLI
→ Audit required? CLI bridge with structured logger
def dispatch(operation):
if operation.type in ("query", "search", "read"):
return call_mcp(operation) if has_tool(operation) else run_raw(operation)
else:
return run_raw_rtk(operation) # CLI with audit logging
This runs in production for every session. The gateway routes, the MCP servers serve, and the CLI bridge handles everything else with logging.
What We Learned
Six focused MCP servers beat one general one. Our ~8K total schema overhead is less than a single GitHub MCP server at ~14K. Fewer tools per server means the agent carries less dead weight on every request.
The gateway pattern is the right default. Near-zero overhead (~20 tokens/prompt). MCP-quality outputs. Linear scaling. No single-server blob.
And the rule that handles 95% of decisions: MCP for structure, CLI for execution. Build tools that return answers, not documents. Run commands that do things, not queries that return everything.
The Closing
The MCP backlash is real, but the target is wrong. The problem isn't the protocol — it's installing everything into one process. A plugin crashes with its host. An MCP server survives, restarts, and waits for the next client.
Lifecycle independence isn't a nice-to-have. It's the feature that makes the rest possible.
Stack details: Lifecycle managed via the oh-my-mcp (port 8080); CLI wrapping, RTK filtering, and audit logging handled by commands-rtk. Core `ark- services run in server/` as standalone MCP servers, not plugins.*
Top comments (0)