Skip to main content

SPEC-091 v0.2: Cursor Streamable HTTP MCP Transport for Interrupt-Grade Signal Delivery

Status: draft
Author: 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, the ChannelsPushStrategy 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/updated over stdio: Cursor ignores server-initiated notifications on stdio transport entirely.
  • notifications/tools/list_changed over stdio: same result — dead letter.
Root cause: In stdio mode, the JSON-RPC protocol is strictly request-response from the client’s perspective. Cursor does not read stdout except in response to its own requests. Notifications emitted “spontaneously” by the server have no delivery path to the model.

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_support per 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/channel on 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.ps1 stale root cleanup)

4.2 Loopback Bind Only

The HTTP server binds exclusively to 127.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_KEY used for the backend

4.4 Session Mapping

  • HTTP clients must still call prism_start to 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_support reverts from full to degraded for the session
  • Diagnostics clearly report transport mode
  • No silent degradation — prism_runtime_diagnostics reports transport: "http" or transport: "stdio"
Surface capability is per-session, not per-surface-string:
  • 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 mirrors channel_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_pending is 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_sent event with result (rang / coalesced / filtered / uncaptured / send-failed), signal_id, signal_type, and trace_id via noteSurfaceDoorbell().

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:
FieldValue
wake_attempt_countIncremented on each rang outcome
last_wake_atTimestamp of the push
last_wake_path"sse" (new path alongside "channel" for Claude Code)
last_wake_result"rang" / "coalesced" / "filtered" / "send-failed"
Coalesced and filtered outcomes increment the counter but record the non-delivery result, preserving observability without inflating “successful wake” metrics.

5.2 Dynamic Surface Support

SPEC-088’s derive_surface_support() currently uses a static surface-string map:
  • claude_code, codex"full"
  • cursor, claude_desktop"degraded"
With SPEC-091, this becomes session-aware:
if surface == "cursor" and session.transport == "http" and session.sse_healthy:
    return "full"
elif surface == "cursor":
    return "degraded"  # stdio fallback
The backend obligation service uses the session-level capability (not the static map) when:
  • Setting obligation SLA exemptions
  • Bucketing SLO metrics
  • Determining max_wake_attempts

5.3 Obligation Lifecycle by Signal Type

Signal Typeaction_requiredreply_requiredDoorbellObligation Terminal
QuestiontruetrueRingsOn reply signal
TaskAssignedtruetrueRingsOn TaskCompleted reply
ReviewRequestedtruetrueRingsOn ReviewCompleted reply
StatusUpdatefalsefalseRingsOn drain
AcknowledgmentfalsefalseRingsOn drain
PeerJoinedFilteredNo obligation
PeerLeftFilteredNo obligation
MasterPreemptedFilteredNo 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/updated on a timer or external trigger
  • Cursor configured via url to connect
  • Observe: does the notification reach the model? Does Cursor re-read the resource? Does it trigger any agent behavior?
Success criteria: Cursor demonstrably receives and acts on a server-pushed notification.
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-node as a second transport face
  • Implement Cursor SSE Bridge (cursor_bridge.ts) mirroring channel_bridge.ts invariants
  • 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 install wires Cursor config to url pointing at daemon
  • Dynamic surface_support reporting on session registration

Milestone 4: Observability + Hardening

  • Metrics: transport type, push attempted/observed/acked, delivery latency
  • Obligation SLA tracking for Cursor sessions (now full support)
  • Fallback detection and auto-switch (surface_support demotion on SSE loss)
  • Andora security review (auth boundary, attack surface)

7. Cursor Config (Target State)

{
  "mcpServers": {
    "prism": {
      "url": "http://127.0.0.1:{port}/mcp",
      "headers": {
        "Authorization": "Bearer {local_token}"
      }
    }
  }
}
Versus today’s stdio config:
{
  "mcpServers": {
    "prism": {
      "command": "node",
      "args": ["C:\\DATA\\Prism\\cli\\dist\\mcp-launcher.js"],
      "env": { ... }
    }
  }
}

8. Risks

RiskMitigation
Cursor may not fully implement Streamable HTTP notification semanticsMilestone 1 spike proves this before any broad work
Port lifecycle / stale daemonReuse codex app-server reaping patterns from coder.ps1
Local HTTP expands attack surface vs stdioLoopback-only bind + token auth + Andora four-eyes review
Session management complexityHTTP clients follow same prism_start/prism_wrap lifecycle
Daemon adds operational complexityClear health endpoint, lockfile, diagnostics
SPEC-088 obligation wake telemetry adds couplingCursor SSE Bridge mirrors channel_bridge.ts contract — same diagnostics, same coalescing, same reset — minimizing new surface area
Dynamic surface_support complicates SLO bucketingPer-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
Last modified on June 7, 2026