MCPorter Daemon Plan
#Goals
- Invisible keep-alive:
mcporter callshould transparently start (and reuse) a per-login daemon whenever a configured server requires persistence (e.g.,chrome-devtools). No extra flags for agents. - Shared state: Multiple CLI invocations/agents within the same user session must reuse the same warm transport so STDIO servers can hold tabs, cookies, and other stateful context.
- Per-config scope: The daemon lives under the current user account, keyed by config path (
~/.mcporter/daemon/daemon-<config-hash>.sockon Unix-like systems, or$XDG_STATE_HOME/mcporter/daemon/...when set), and never crosses user boundaries. - Resilience: If the daemon or a keep-alive server crashes, the next CLI call restarts it automatically.
- Explicit shutdown: Provide
mcporter daemon stopto tear everything down (plusstatusfor debugging). - Configurable participation: Only servers marked keep-alive participate; others keep current ephemeral behavior. Support opt-in/out via config/env plus a default allowlist.
#Architecture
- Daemon process (
mcporter daemon start): - Loads the same config as the CLI.
- Hosts a long-lived
McpRuntime. - Listens on a Unix domain socket (per-login path, chmod 600).
- Exposes a minimal JSON-RPC interface that mirrors the existing
list/call/resourcesAPIs so CLI commands can proxy requests. - Lazily connects keep-alive servers on first use and keeps transports open until shutdown or idle timeout.
- Client shim (CLI side):
- When a command targets a keep-alive server:
- Look for a ready daemon socket; if missing, spawn
mcporter daemon start --detach. - Proxy the list/call/auth request over the socket and print the response as usual.
- If the socket handshake fails (daemon crashed mid-call), re-spawn once before surfacing the error.
- Non keep-alive servers continue using the local runtime in the current process.
- Keep-alive detection:
- Extend
ServerDefinitionwithlifecycle?: "ephemeral" | { mode: "keep-alive", idleTimeoutMs?: number }. - Provide a config-level
defaultKeepAlivearray orMCPORTER_KEEPALIVEenv var for quick overrides. - Ship a hardcoded allowlist (initially
chrome-devtools,mobile-mcp,playwright) so existing configs benefit immediately; users can opt out per server.
#CLI Surface
mcporter daemon start [--foreground]: boot the daemon; default behavior is background (detached) launch that writes its metadata file under the daemon runtime directory.mcporter daemon status: show whether the daemon is running, the socket path, uptime, and which servers are currently connected/idle.mcporter daemon stop: instruct the daemon to close all transports and remove its socket/metadata; if the daemon is missing, exit 0 with a hint.mcporter daemon restart: convenience wrapper that stops the daemon (if it exists), waits for the socket to disappear, and launches a fresh instance while reusing the same logging flags/env overrides.- Existing commands (
list,call,auth,emit-ts, etc.) continue to work; only those touching keep-alive servers will route through the daemon.
#Lifecycle & Fault Handling
- Auto start: First call requiring the daemon triggers a lightweight bootstrap (fork/exec via
child_process.spawninside the CLI). We ensure the original command waits for the socket to become available (with a short timeout). - macOS Bun binaries: Homebrew/Bun-compiled binaries wrap the detached child launch with
nohupso the background daemon survives the parent CLI exit on macOS 26. - Auto restart: The client shim treats
ECONNREFUSED/broken pipe as a signal that the daemon died. It retries once by re-launching the daemon before surfacing the error. - Idle timeout: Each keep-alive server can specify
idleTimeoutMs(defaultnull= never). The daemon tracks last activity timestamps and auto-closes transports (and associated external processes) after the idle window. A globaldaemonIdleTimeoutMscan shut down the entire daemon after long inactivity. - Logging: Daemon writes structured logs under the daemon runtime directory plus per-server logs for STDIO stderr so users can debug crashing servers.
#Agent Isolation
By default, multiple agents using the same config path share the same keep-alive daemon. That is deliberate: stateful servers such as browser or device MCPs can keep tabs, sessions, and subprocesses warm across repeated CLI calls.
If each agent needs independent MCP state, give each agent either:
- a distinct
--config <path>/MCPORTER_CONFIGvalue, which produces a distinct daemon socket and metadata file; or - a distinct
MCPORTER_DAEMON_DIR, which isolates the whole daemon runtime directory even when the config path is shared. This explicit override wins overXDG_STATE_HOME.
Non-keep-alive servers remain process-local and do not use the daemon.
#Testing Plan
- Unit tests
- Config parsing for the new
lifecycleshape and env overrides. - Daemon controller: socket path resolution, metadata persistence, auto-restart logic.
- Integration tests (Vitest)
- Spin up a fake STDIO MCP server (script under
tests/fixtures/daemon-server.ts) that increments a counter so we can assert the transport stays alive across multiple CLI invocations. - Verify
mcporter callauto-starts the daemon, reuses the server, andmcporter daemon stopshuts it down. - Simulate daemon crash by killing the background process and ensure the next call restarts it automatically.
#Implementation Steps
- Config/schema changes: Update
src/config.tsplus fixtures to acceptlifecycle. Provide helpers likerequiresKeepAlive(definition). - Daemon service: New module (e.g.,
src/daemon/host.ts) that runs the socket server, wrapsMcpRuntime, and exposes RPC handlers. - CLI wiring: Add
daemonsubcommand + option parsing; create a client helperensureDaemon()used bycall/listpaths when a keep-alive server is detected. - Transport proxying: Implement request/response translation so CLI commands can await daemon responses as if they were local.
- Auto-detection + env overrides: Hook into command selectors to decide when to proxy.
- Tests + docs: Add Vitest coverage, update README/cli reference snippets, and keep this doc synced with actual behavior.
#Logging & Diagnostics
You can capture the daemon’s stdout/stderr (and per-server call traces) when debugging long-lived STDIO servers:
mcporter daemon start --logenables logging with the default path~/.mcporter/daemon/daemon-<config-hash>.log, or$XDG_STATE_HOME/mcporter/daemon/daemon-<config-hash>.logwhenXDG_STATE_HOMEis set. Use--log-file <path>to override it.--log-servers chrome-devtools,mobile-mcprestricts per-call logging to the listed servers. Without it,--logrecords every keep-alive server’s activity.- Environment equivalents:
MCPORTER_DAEMON_LOG=1– enable logging.MCPORTER_DAEMON_LOG_PATH=/tmp/mcporter-daemon.log– explicit log file.MCPORTER_DAEMON_LOG_SERVERS=chrome-devtools– only log specified servers.mcporter daemon statusnow prints the socket path and the active log file (if any) so it’s easy to tail.- Per-server opt-in: add
"logging": { "daemon": { "enabled": true } }next to"lifecycle": "keep-alive"in a server definition to force detailed call logging for that server (handy when only one or two STDIO transports are noisy). Combined with--log/MCPORTER_DAEMON_LOG, those entries always emit call start/end/error lines.
Logs include timestamped entries such as:
[daemon] 2025-11-10T15:08:21.123Z callTool start server=chrome-devtools tool=take_snapshot
[daemon] 2025-11-10T15:08:22.004Z callTool success server=chrome-devtools tool=take_snapshot
Tailing the file (tail -f ~/.mcporter/daemon/daemon-*.log, or the matching XDG state path) surfaces crashes or repeated failures without needing to re-run the daemon in the foreground.
Once these steps land, agents can freely use persistent MCP servers without juggling multiple Chrome launches, while still retaining an explicit shutdown path.