v3.2.0 — leadership of :4050 becomes a lease
Added
-
A consent wait now reaches the client as progress. The extension's approval modal has a five-minute budget; most IDEs time a tool call out at sixty seconds. Until now the bridge extended its own timer on every
dispatch_tool_pendingframe and told the client nothing, so a user who took ninety seconds to approve on their phone approved into a void: the client had reported the call failed, the tool then ran, and the agent re-issued it. Every pending frame is now anotifications/progresson requests that carried aprogressToken, on both the leader and the follower door. Clients that reset their timeout on progress (the MCP SDK from 1.9, Claude Code) wait with us. Claude Desktop and Cursor cap at a fixed sixty seconds regardless at the time of writing, socall_webmcp_tool's description now tells the agent what a timeout there does and does not mean, and not to blindly re-issue a call with side effects. -
customaise schemaprints the command tree as JSON: every verb, the flags it takes, the MCP tool it reaches, and the exit codes. An agent discovers a CLI by asking it. A test fails if the tree and the dispatcher disagree, or if it names a tool the server does not register. -
A modern client appears in the extension's client list from its first request. On the 2026-07-28 revision a client's name rides
_metaon every request, and it was read only inside tool calls, so a client that had listed tools and not yet called one had no row: Claude Desktop, freshly launched, lists tools and resources and then waits for a human, and its row was absent until the first tool call. Identity is now read from any inbound request on either door. -
The CLI daemon appears in the extension's client list by name. It never runs MCP
initialize, which is where an editor's identity comes from, so it held the port invisibly: Settings showed every editor as(follower)and nothing as the leader. It now shows ascustomaise daemon, and the extension (1.3.2) labels the leader row(leader)rather than leaving it as the one with no tag. -
Noun-verb forms for the three short verbs:
tab list,tab shot,tab usealongsidetabs,shot,use, so every tab operation lives under one noun in--helpand in the schema.-hnow works as well as--help.
Changed
- Error codes moved out of the range the specification reserved. The 2026-07-28 revision claims
-32020..-32099for itself ("Implementations MUST NOT emit any code from this sub-range that is not defined by this specification"), and ours sat at-32028..-32033with the spec's own allocations walking toward them. They are now-40028..-40033, same last digits. Nothing branches on the number (the CLI and the envelope key onerror.type, which did not change), and the server accepts the old values from extensions and leaders built before the move, so a mixed fleet shows one number per condition. Ships with extension 1.3.2, which emits the new values.
Fixed
-
A process that loses the leader of
:4050now takes the port itself. Only onecustomaise-mcpbinds the extension's WebSocket port; the rest follow it. Leadership was decided once at startup and never revisited, which left two holes with one root. A process that lost the bind and dialled a leader that had already exited died with "connection closed before handshake", which Claude Desktop reported asVersion negotiation failedand left the Cowork and Code sessions without Customaise until the app was restarted. A process whose leader exited AFTER greeting it lived on as an orphan, dialling a dead port forever, while nobody re-bound it; the extension gives up after three reconnect attempts, so the user saw MCP "randomly disconnect" and had to toggle it by hand. Claude Desktop hits both routinely: it spawns a disposable copy of this server purely to probe its protocol revision and reaps it a second later, and on a cold boot the probe runs slowly enough for a real server to lose the bind race to it.createBridgenow returns anElectingBridgethat races the port at startup and again the moment a follower's leader goes away, promoting itself if the port is free and rejoining whoever won it otherwise. The bind is the arbiter, so several followers losing one leader converge on exactly one. A tool call that lands mid-election waits for it rather than failing, and a call that was in flight when the leader died now fails with the typedleader_unreachable(CLI exit 3) instead of a bare error (exit 1). A follower evicted over a relay-protocol mismatch still fails at once with the leader's own explanation rather than waiting out an election that would only be evicted again. The 3.0.x reconnect loop insideRemoteBridge, which could dial but never bind, is gone; its comment had argued promotion was unnecessary because "if no leader ever comes back there is nothing to talk to anyway", which is false whenever the leader that died was the only other process. -
A leader steps down for a newer package. A resident daemon idle-exits only when no extension is attached, so with Chrome open it held
:4050indefinitely: afternpm i -g @customaise/mcp@<newer>, every editor spawned the new version, lost the bind to the old daemon, and followed it, while Settings showed "update available" for an update the user had already installed. Onlycustomaise daemon stopcleared it. Every follower's hello already carried its package version; a leader that sees a strictly newer one, once that follower has identified itself as a client (Claude Desktop's disposable protocol-probe copy of this server never does, and yielding to it meant two hand-overs per Desktop update), now releases the port with a close code naming that version, the other followers hold back 300ms so it binds first, and the old leader rejoins last as a follower. If nothing binds, the election takes the port straight back, so stepping down never leaves the seat empty, and a leader that stepped down for a version that then failed to take the port will not do so again for that version for a minute, so a newer process that cannot bind leaves an older leader in place rather than an extension that is disconnected more often than not. A hand-over that succeeded is never rate-limited: if the newer process restarts and the older one wins the bind in between, it yields again at once. The version check runs before the relay-protocol check, so a newer package that also changed the relay protocol is handed the port rather than evicted with advice to restart a daemon that never exits. Prerelease tags compare equal to their base, so two builds of one release never trade the port back and forth. The extension sees the hand-over as one reconnect, a few seconds, once per update. -
A consent modal no longer outlives the call it was asking about. When the bridge closed with a dispatch still waiting on the user (an IDE quitting, or now a step-down), the extension was never told, so the modal stayed on screen for its full five minutes asking about a call nobody was coming back for.
close()now sendscancel_dispatchfor every pending call before the socket goes, the same frame an aborted call already sends, and the modal closes with it. -
A follower evicted over a relay-protocol mismatch fails at once with the leader's explanation, rather than waiting out an election that would only be evicted again. A call in flight when the leader died now fails with the typed
leader_unreachable(CLI exit 3) instead of a bare error (exit 1).
Extension side (1.3.2)
- Reconnect heartbeat 5 minutes → 30 seconds. The extension retries a dropped bridge three times over 35 seconds, then falls back to a
chrome.alarmsheartbeat, which is the only timer that survives the service worker being torn down. At five minutes, quitting one IDE and opening another a minute later left MCP showing disconnected for up to four more. Thirty seconds is the alarm floor on Chrome 120, the extension's minimum; one failed TCP connect per thirty seconds while nothing is listening costs nothing.