How We Made Stateful MCP Servers Work in One-Shot CLI Calls
The Problem: Stateful Servers vs Stateless CLI
Context-mode is a powerful MCP (Model Context Protocol) server that maintains a SQLite knowledge base and session history. It reduces AI context window usage by up to 98% by keeping raw tool output out of conversations and only returning relevant snippets.
But there was a problem: context-mode is stateful. It needs to stay alive between calls to maintain its knowledge base. Traditional CLI tools are stateless — each invocation is a fresh process.
When we tried to integrate context-mode into supercli (a CLI wrapper for external tools), we hit a wall:
# Call 1: Index project structure
sc mcp call --mcp-server context-mode --tool ctx_index --input-json '{"content":"..."}'
# Call 2: Search — but knowledge is gone! New process, new database.
sc mcp call --mcp-server context-mode --tool ctx_search --input-json '{"queries":["routes"]}'
Each sc call spawned a new context-mode process. The knowledge base was lost between calls. Agents using supercli in one-shot CLI invocations couldn't benefit from context-mode's stateful features.
The Solution: MCP Daemon
We built a daemon process manager that keeps MCP servers alive persistently across CLI invocations.
Architecture
┌─────────────────────────────────────────────────────────┐
│ CLI Call (one-shot) │
│ sc mcp call --mcp-server context-mode --tool ctx_search │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ MCP Daemon (persistent background process) │
│ Unix socket: ~/.supercli/mcp-daemon.sock │
│ Process pool: Map<<serverName, ChildProcess>> │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ context-mode (long-lived process) │
│ SQLite knowledge base persists across CLI calls │
└─────────────────────────────────────────────────────────┘
Key Features
-
Unix Socket Server: Daemon listens on
~/.supercli/mcp-daemon.sockfor JSON-RPC requests - Process Pool: Maintains long-lived child processes for each MCP server
- Auto-Start: Daemon starts automatically on first tool call if not running
-
Stateful Flag: MCP servers can be marked as
stateful: trueto use the daemon - Graceful Shutdown: Cleanly stops all MCP processes on daemon exit
Implementation
Daemon (cli/mcp-daemon.js):
// Unix socket server
const server = net.createServer((socket) => {
socket.on('data', (chunk) => {
const req = JSON.parse(chunk);
if (req.method === 'call_tool') {
const entry = await getOrSpawnServer(req.params.server, req.params.serverConfig);
const result = await callTool(entry, req.params.tool, req.params.input);
socket.write(JSON.stringify({ id: req.id, result }) + '\n');
}
});
});
server.listen(SOCKET_PATH);
Client (cli/mcp-daemon-client.js):
async function callDaemonTool({ server, serverConfig, tool, input, timeout_ms }) {
if (!await isDaemonRunning()) {
await startDaemon(); // Auto-start if not running
}
return sendDaemonRequest('call_tool', { server, serverConfig, tool, input, timeout_ms });
}
MCP Adapter (cli/adapters/mcp.js):
if (config.stateful) {
// Route through daemon instead of spawning new process
return callDaemonTool({ server, serverConfig, tool, input, timeout_ms });
} else {
// Existing behavior: spawn per call
return stdioCallToolJsonRpc({ ... });
}
Usage
Installation
npm install -g context-mode
sc plugins install ./plugins/context-mode --on-conflict replace --json
# Automatically registers as stateful MCP server + starts daemon
CLI Commands
# Daemon management
sc mcp daemon start # Start daemon
sc mcp daemon status # Check status + active servers
sc mcp daemon stop # Stop daemon
sc mcp daemon restart # Restart daemon
# Add stateful MCP server
sc mcp add my-server --command my-tool --stateful
# Call context-mode tools (knowledge persists across calls)
sc mcp call --mcp-server context-mode --tool ctx_batch_execute --input-json '{
"commands": [
{"label": "Package", "command": "cat package.json"},
{"label": "Source tree", "command": "find src -name \"*.js\" | head -40"}
],
"queries": ["entry point", "dependencies"]
}'
Benefits
1. Knowledge Base Persistence
# Call 1: Index docs
sc mcp call --mcp-server context-mode --tool ctx_index --input-json '{
"content": "...large documentation...",
"source": "api-docs"
}'
# Call 2 (new shell, new process): Knowledge persists!
sc mcp call --mcp-server context-mode --tool ctx_search --input-json '{
"queries": ["authentication", "token expiry"]
}'
2. Context Window Reduction
ctx_batch_execute runs commands, auto-indexes output, and searches in one call — returning only matched snippets, not raw output:
Executed 3 commands (57 lines, 1.4KB). Indexed 3 sections. Searched 4 queries.
## dependencies (only relevant snippets returned)
### Package
{
"dependencies": {
"dotenv": "^17.3.1",
"ejs": "^3.1.9",
"express": "^4.18.2",
"mongodb": "^6.3.0"
}
}
3. Agent-Friendly
Agents call supercli as one-shot commands. The daemon ensures knowledge base persistence:
# Agent call 1
sc mcp call --mcp-server context-mode --tool ctx_batch_execute ...
# Agent call 2 (different process, different shell)
sc mcp call --mcp-server context-mode --tool ctx_search ...
# Knowledge base still available via daemon
4. Zero Setup for Agents
The daemon auto-starts on first tool call. Agents don't need to manage it:
# First call: daemon auto-starts
sc mcp call --mcp-server context-mode --tool ctx_doctor --input-json '{}'
# Subsequent calls: daemon already running
sc mcp call --mcp-server context-mode --tool ctx_search ...
Technical Details
Protocol
Daemon uses JSON over Unix socket:
// Request
{"id": 1, "method": "call_tool", "params": {"server": "context-mode", "tool": "ctx_search", "input": {...}}}
// Response
{"id": 1, "result": {...}}
Process Lifecycle
- Daemon starts (detached process, no TTY)
- First tool call → daemon spawns context-mode process
- JSON-RPC handshake (initialize → notifications/initialized)
- Tool calls routed through daemon
- Process stays alive for subsequent calls
- Daemon shutdown → graceful termination of all MCP processes
Wire Mode
We discovered context-mode uses JSONL (newline-delimited JSON) not LSP framing. The daemon defaults to JSONL for stdio MCP servers:
const entry = {
wireMode: serverConfig.wire_mode || "jsonl", // Default to JSONL
...
};
Future Directions
Now that we have the daemon infrastructure, we can explore:
- Automatic output reduction: Process adapter could pipe output through context-mode automatically
- Ask command caching: LLM suggestions cached in knowledge base
- Workflow context: Cross-step knowledge indexing
- Error learning: Pattern-based error solutions
- Server mode shared KB: Collective intelligence across API clients
Conclusion
The MCP daemon enables stateful MCP servers like context-mode to work seamlessly in one-shot CLI environments. Agents get full context-mode benefits (98% context reduction, knowledge base persistence) while supercli remains a clean, stateless CLI wrapper.
The architecture is extensible — any stateful MCP server can now integrate with supercli via the daemon, opening possibilities for persistent tool sessions across CLI invocations.
Try it out:
npm install -g context-mode
sc plugins install ./plugins/context-mode --on-conflict replace --json
sc mcp daemon status
sc mcp call --mcp-server context-mode --tool ctx_doctor --input-json '{}'
Top comments (0)