SPEC-091 v0.2: Cursor Streamable HTTP MCP Transport for Interrupt-Grade Signal Delivery
Status: draftAuthor: Cherry
Reviewer: Texi (architecture), Andora (security), Jade (SPEC-088 compatibility)
Date: 2026-05-07 (v0.1), 2026-05-09 (v0.2)
Scope: Cursor only — no cross-editor migration
1. Problem Statement
Prism’s signal mesh delivers signals to the MCP server process in real-time via WebSocket (stream.ts). For Claude Code, theChannelsPushStrategy fires a proprietary notifications/claude/channel notification that wakes the agent instantly. For Cursor, signals buffer in memory (PiggybackStrategy) and only reach the model on the next tool call.
The gap is the last mile: MCP server process → Cursor model. The signal is already at the door, but there is no bell to ring.
SPEC-088 Phase A (Signal Obligations) now tracks obligation lifecycle with wake telemetry fields (wake_attempt_count, last_wake_path, last_wake_result). Cursor surfaces are classified surface_support: "degraded" because no push path exists. This spec closes that gap.
What Was Tested and Failed
notifications/resources/updatedover stdio: Cursor ignores server-initiated notifications on stdio transport entirely.notifications/tools/list_changedover stdio: same result — dead letter.
2. Proposed Solution
Add a Streamable HTTP transport face to the Prism MCP server, specifically for Cursor. The MCP spec (2025-11-25) explicitly supports server-to-client push via a GET SSE stream.Architecture Overview
Signal Delivery Flow — Question Signal (Donna → Sable)
Signal Delivery Flow — TaskAssigned Signal (Donna → Sable)
Signal Delivery Flow — StatusUpdate Signal (Donna → Sable)
Signal Delivery Flow — System Broadcast (PeerJoined/PeerLeft)
Signal Delivery Flow — Coalesced Burst (Donna → Sable, multiple signals)
Comparison: Stdio vs Streamable HTTP
3. Scope Boundaries
In Scope
- Cursor signal delivery only
- Local Streamable HTTP/SSE transport face
- Cursor SSE Bridge with doorbell/coalescing semantics (mirrors
channel_bridge.ts) - SPEC-088 obligation wake telemetry integration
- Dynamic
surface_supportper session transport - Daemon-owned listener process
- Auth boundary for local loopback
- Proof-of-delivery spike (Milestone 1)
- Fallback to stdio+piggyback if HTTP unavailable
Out of Scope
- Claude Code signal delivery (stays
notifications/claude/channelon stdio) - Codex signal delivery (stays app-server inject)
- Claude Desktop (stays piggyback on stdio)
- Cross-editor transport migration
- Remote/cloud HTTP deployment (LAN hosting deferred to separate spec)
- gRPC CoordinationStream changes
4. Design Decisions
4.1 Daemon-Owned Listener
The HTTP server is owned by a daemon process, not spawned per-editor-window. Rationale (per Texi):- Port lifecycle, single-instance locking, health checks, stale cleanup need daemon semantics
- Otherwise every editor window becomes a port/process ownership bug
- Reuse lessons from Codex app-server process reaping (
coder.ps1stale root cleanup)
4.2 Loopback Bind Only
The HTTP server binds exclusively to127.0.0.1. No network-accessible port. This is a local-machine-only transport. LAN hosting (server1.home.lan) is architecturally simpler but changes the trust boundary and session ownership model — deferred to a follow-on spec after Milestone 1 proves Cursor honors SSE notifications.
4.3 Auth Boundary
Even on loopback, MCP tools must not be exposed unauthenticated. The HTTP face uses:- A shared local secret (written to
~/.prism/http-mcp-token) generated at daemon start - Cursor config includes the token in headers
- Alternatively: inherit the same
PRISM_API_KEYused for the backend
4.4 Session Mapping
- HTTP clients must still call
prism_startto register a controller session - The HTTP transport does not create phantom registrations
- One Cursor window = one Prism session (same as stdio today)
4.5 Fallback and Dynamic Surface Capability
If the HTTP daemon is unreachable:- Cursor config falls back to stdio+piggyback
surface_supportreverts fromfulltodegradedfor the session- Diagnostics clearly report transport mode
- No silent degradation —
prism_runtime_diagnosticsreportstransport: "http"ortransport: "stdio"
- Cursor + active SSE stream →
surface_support: "full" - Cursor + stdio only (fallback) →
surface_support: "degraded" - Backend obligation SLO bucketing uses session-level capability, not the static surface map
4.6 Cursor SSE Bridge (Doorbell Semantics)
The Cursor SSE Bridge mirrorschannel_bridge.ts invariants from SPEC-044 §3.2/§3.3:
- Doorbell, not payload. The SSE notification contains no signal content. It is a wake event that tells the model to call
prism_signals_pending. The authoritative payload always comes from the drain verb. - One bell per drain cycle. At most one outstanding SSE notification. If a second signal arrives before the model drains, it is coalesced — no additional SSE event fires.
- Reset on drain. When
prism_signals_pendingis called and returns results, the coalescing flag resets. The next signal arrival can ring again. - System type filtering.
PeerJoined,PeerLeft,MasterPreempted(SYSTEM_TYPES) never ring the doorbell. They reach the signal cache for drain but do not wake the model. - Diagnostics on every push outcome. Every push attempt records a
surface_doorbell_sentevent with result (rang/coalesced/filtered/uncaptured/send-failed), signal_id, signal_type, and trace_id vianoteSurfaceDoorbell().
5. SPEC-088 Integration (Signal Obligations)
5.1 Wake Telemetry
When the Cursor SSE Bridge pushes a doorbell, it reports the wake attempt to the backend:| Field | Value |
|---|---|
wake_attempt_count | Incremented on each rang outcome |
last_wake_at | Timestamp of the push |
last_wake_path | "sse" (new path alongside "channel" for Claude Code) |
last_wake_result | "rang" / "coalesced" / "filtered" / "send-failed" |
5.2 Dynamic Surface Support
SPEC-088’sderive_surface_support() currently uses a static surface-string map:
claude_code,codex→"full"cursor,claude_desktop→"degraded"
- Setting obligation SLA exemptions
- Bucketing SLO metrics
- Determining max_wake_attempts
5.3 Obligation Lifecycle by Signal Type
| Signal Type | action_required | reply_required | Doorbell | Obligation Terminal |
|---|---|---|---|---|
| Question | true | true | Rings | On reply signal |
| TaskAssigned | true | true | Rings | On TaskCompleted reply |
| ReviewRequested | true | true | Rings | On ReviewCompleted reply |
| StatusUpdate | false | false | Rings | On drain |
| Acknowledgment | false | false | Rings | On drain |
| PeerJoined | — | — | Filtered | No obligation |
| PeerLeft | — | — | Filtered | No obligation |
| MasterPreempted | — | — | Filtered | No obligation |
6. Milestones
Milestone 1: Proof-of-Delivery Spike
Goal: Prove Cursor reacts to server-initiated SSE notifications over Streamable HTTP. Deliverables:- Minimal Streamable HTTP MCP server (loopback, one tool, one resource)
- Sends
notifications/resources/updatedon a timer or external trigger - Cursor configured via
urlto connect - Observe: does the notification reach the model? Does Cursor re-read the resource? Does it trigger any agent behavior?
Failure criteria: Cursor ignores SSE notifications → stop here, document limitation, remain on piggyback.
Milestone 2: Integration with Prism Signal Path
- Wire the HTTP server into
mcp-nodeas a second transport face - Implement Cursor SSE Bridge (
cursor_bridge.ts) mirroringchannel_bridge.tsinvariants - Backend WS → stream.ts → Cursor SSE Bridge → HTTP/SSE push
- Full verb surface over HTTP (same 85+ tools)
- Session lifecycle parity with stdio
- Wake telemetry reporting to SPEC-088 obligations
Milestone 3: Daemon Lifecycle
- Daemon startup/shutdown/lockfile/health
- Port registration in
~/.prism/http-mcp.json - Stale process reaping
prism installwires Cursor config tourlpointing at daemon- Dynamic
surface_supportreporting on session registration
Milestone 4: Observability + Hardening
- Metrics: transport type, push attempted/observed/acked, delivery latency
- Obligation SLA tracking for Cursor sessions (now
fullsupport) - Fallback detection and auto-switch (surface_support demotion on SSE loss)
- Andora security review (auth boundary, attack surface)
7. Cursor Config (Target State)
8. Risks
| Risk | Mitigation |
|---|---|
| Cursor may not fully implement Streamable HTTP notification semantics | Milestone 1 spike proves this before any broad work |
| Port lifecycle / stale daemon | Reuse codex app-server reaping patterns from coder.ps1 |
| Local HTTP expands attack surface vs stdio | Loopback-only bind + token auth + Andora four-eyes review |
| Session management complexity | HTTP clients follow same prism_start/prism_wrap lifecycle |
| Daemon adds operational complexity | Clear health endpoint, lockfile, diagnostics |
| SPEC-088 obligation wake telemetry adds coupling | Cursor SSE Bridge mirrors channel_bridge.ts contract — same diagnostics, same coalescing, same reset — minimizing new surface area |
| Dynamic surface_support complicates SLO bucketing | Per-session capability is strictly more accurate than static map; degraded sessions still exempt from primary SLO |
9. References
- MCP Spec (2025-11-25): Streamable HTTP Transport — https://modelcontextprotocol.io/specification/latest/basic/transports
- SPEC-044: Channel-Based Signal Push (Claude Code) — doorbell/coalescing invariants
- SPEC-046: Agent Surface Adapters
- SPEC-069: Minimal Actionable Verb Set — doorbell content contract
- SPEC-070: Per-Agent Persona Daemon
- SPEC-088: Signal Obligations — wake telemetry, surface_support, obligation lifecycle
- ADR-39: Installation/Config Ownership
- Branch
cherry/cursor-signal-doorbell: failed stdio notification test (retained for reference) - Jade review (2026-05-09): APPROVE_WITH_REQUIRED_REVISIONS — SPEC-088 integration, dynamic capability, doorbell strategy

