Problem
The daemon persists across CLI invocations (ADB-style, per AGENTS.md). When the user upgrades previewsmcp (via Homebrew, `brew upgrade` / reinstall; or via `swift build` / source), the new CLI binary starts talking to the old daemon that's still running. The daemon was booted from the previous binary version and stays alive until manually killed, rebooted, or explicitly told to exit.
Symptoms:
- New CLI features that depend on new daemon behaviors silently misbehave.
- Internal protocol tweaks (e.g., the v0.12.0 heartbeat channel and stall detection) can mismatch between versions; the CLI's stall timer may trip on an old daemon that doesn't emit heartbeats.
- Error messages shaped for the new daemon don't match old-daemon responses.
Reproducer
# Install old version
brew install obj-p/tap/previewsmcp # say, v0.11.3
# Start daemon (any CLI command does this transparently)
previewsmcp status
# Upgrade
brew upgrade obj-p/tap/previewsmcp # to v0.12.0
# CLI is new, daemon is still old:
previewsmcp --version # shows 0.12.0
ps aux | grep "previewsmcp serve --daemon" # shows process started by 0.11.3 binary
# User commands now go to the stale daemon:
previewsmcp logs # new subcommand — works, since it's CLI-only
previewsmcp run ... # goes to old daemon — may miss new features or trip stall timer
Options, ranked
1. Version handshake in the daemon's MCP initialize response (recommended)
Daemon embeds its binary version in its MCP `serverInfo.version` (already the case via PreviewsMCPCommand.version). On connect, the CLI compares server.serverInfo.version against its own compile-time version. On mismatch:
- CLI logs to stderr: "upgraded: restarting daemon (was X, now Y)"
- CLI calls `kill-daemon`, polls for shutdown
- CLI spawns a fresh daemon with the new binary
- CLI reconnects
This covers every install path (Homebrew, SPM, source build, IDE).
2. Homebrew post-install hook
Formula runs `previewsmcp kill-daemon` on upgrade. Clean for Homebrew users, but doesn't help anyone else. Useful as a secondary defense even with (1) in place — short-circuits the version check since the daemon is already dead before the new CLI connects.
3. Startup banner / warning only
On version mismatch, print a warning and continue. Low-effort, but requires users to act — most won't.
Why this matters more now
v0.12.0 introduces client-server protocol coupling that v0.11.3 didn't have:
- Daemon emits 2s heartbeat `LogMessageNotification` (new)
- CLI's `DaemonClient.withDaemonClient` has a 30s stall timer that expects those heartbeats
- New CLI talking to old daemon: 30s after connect, stall timer trips and disconnects
So v0.12.0 users who auto-upgrade over a running v0.11.x daemon will hit stall-disconnect errors on every command until they manually `kill-daemon`. That's a regression users will hit by simply doing `brew upgrade`.
Suggested scope for the fix
First PR: (1) alone. Handles every install path and fixes the v0.11→v0.12 upgrade specifically.
Follow-up: (2) as defense-in-depth, once the formula is touched for other reasons.
🤖 Filed from a Claude Code session after noticing the symptom on upgrade.
Problem
The daemon persists across CLI invocations (ADB-style, per AGENTS.md). When the user upgrades
previewsmcp(via Homebrew, `brew upgrade` / reinstall; or via `swift build` / source), the new CLI binary starts talking to the old daemon that's still running. The daemon was booted from the previous binary version and stays alive until manually killed, rebooted, or explicitly told to exit.Symptoms:
Reproducer
Options, ranked
1. Version handshake in the daemon's MCP
initializeresponse (recommended)Daemon embeds its binary version in its MCP `serverInfo.version` (already the case via
PreviewsMCPCommand.version). On connect, the CLI comparesserver.serverInfo.versionagainst its own compile-time version. On mismatch:This covers every install path (Homebrew, SPM, source build, IDE).
2. Homebrew post-install hook
Formula runs `previewsmcp kill-daemon` on upgrade. Clean for Homebrew users, but doesn't help anyone else. Useful as a secondary defense even with (1) in place — short-circuits the version check since the daemon is already dead before the new CLI connects.
3. Startup banner / warning only
On version mismatch, print a warning and continue. Low-effort, but requires users to act — most won't.
Why this matters more now
v0.12.0 introduces client-server protocol coupling that v0.11.3 didn't have:
So v0.12.0 users who auto-upgrade over a running v0.11.x daemon will hit stall-disconnect errors on every command until they manually `kill-daemon`. That's a regression users will hit by simply doing `brew upgrade`.
Suggested scope for the fix
First PR: (1) alone. Handles every install path and fixes the v0.11→v0.12 upgrade specifically.
Follow-up: (2) as defense-in-depth, once the formula is touched for other reasons.
🤖 Filed from a Claude Code session after noticing the symptom on upgrade.