> ## Documentation Index
> Fetch the complete documentation index at: https://prism.ntecdev.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Spec 091 v0 1 cursor streamable http signal delivery

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

> **Current authority notice:** This SPEC covers Cursor last-mile wake delivery. The consolidated current Marconi signaling source-of-authority document is [Marconi](https://prism.ntecdev.ai/marconi.md). Diagrams or references to Redis/pubsub here are historical surface-transport context, not current signal authority.

**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

```mermaid theme={null}
flowchart TB
    subgraph backend["Prism Backend (server1.home.lan)"]
        redis["Redis Pub/Sub"]
        ws_forwarder["WS Forwarder"]
        obligations["Signal Obligations<br/>(SPEC-088)"]
    end

    subgraph mcp_daemon["MCP Daemon (localhost)"]
        direction TB
        ws_client["WebSocket Client<br/>(stream.ts)"]
        cursor_bridge["Cursor SSE Bridge<br/>(doorbell + coalescing)"]
        http_server["Streamable HTTP Server<br/>127.0.0.1:{port}/mcp"]
        channel_bridge["Channel Bridge<br/>(existing, unchanged)"]
        stdio_server["Stdio Server<br/>(existing, unchanged)"]
    end

    subgraph cursor["Cursor IDE (mini1)"]
        direction TB
        model["Agent Model (Sable)"]
        post["POST /mcp<br/>(tool calls)"]
        get_sse["GET /mcp<br/>(SSE stream)"]
    end

    subgraph claude_code["Claude Code (mini3, unchanged)"]
        cc_model["Agent Model (Donna)"]
        cc_stdio["stdio pipe"]
    end

    redis --> ws_forwarder
    ws_forwarder -->|"WS frames"| ws_client
    ws_client -->|"signal arrives"| cursor_bridge
    ws_client -->|"signal arrives"| channel_bridge
    cursor_bridge -->|"doorbell (coalesced)"| http_server
    
    http_server -->|"SSE notification push"| get_sse
    post -->|"tool call request"| http_server
    http_server -->|"tool response"| post
    get_sse -->|"notifications/resources/updated"| model

    channel_bridge -->|"notifications/claude/channel"| stdio_server
    stdio_server --> cc_stdio
    cc_stdio --> cc_model

    obligations -.->|"wake telemetry"| ws_forwarder

    classDef backend_style fill:#1e40af,stroke:#1e3a8a,color:#fff
    classDef daemon_style fill:#92400e,stroke:#78350f,color:#fff
    classDef cursor_style fill:#166534,stroke:#14532d,color:#fff
    classDef claude_style fill:#5b21b6,stroke:#4c1d95,color:#fff
    class backend backend_style
    class mcp_daemon daemon_style
    class cursor cursor_style
    class claude_code claude_style
```

### Signal Delivery Flow — Question Signal (Donna → Sable)

```mermaid theme={null}
sequenceDiagram
    participant Donna as Donna<br/>(Claude Code / mini3)
    participant MCP3 as MCP Server<br/>(mcp-node / mini3)
    participant API as Prism Backend<br/>(server1.home.lan)
    participant Oblig as Signal Obligations<br/>(SPEC-088)
    participant Redis as Redis Pub/Sub
    participant WS1 as WS Client<br/>(stream.ts / mini1)
    participant Bridge as Cursor SSE Bridge<br/>(mini1)
    participant HTTP as HTTP/SSE Server<br/>(mini1 :PORT/mcp)
    participant Cursor as Cursor IDE<br/>(mini1)
    participant Sable as Sable<br/>(Cursor Agent)

    Note over Cursor,HTTP: Session Init (one-time)
    Cursor->>HTTP: POST /mcp (InitializeRequest)
    HTTP->>Cursor: InitializeResponse + MCP-Session-Id
    Cursor->>HTTP: GET /mcp (Accept: text/event-stream)
    HTTP-->>Cursor: SSE stream opened (persistent)
    HTTP->>API: Report transport=http, surface_support=full

    Note over Donna,Sable: Donna sends Question to Sable
    Donna->>MCP3: prism_signal(to="Sable", type="Question")
    MCP3->>API: POST /api/v1/signal
    API->>API: Persist to signal_queue (Postgres)
    API->>Oblig: Create obligation (action_required=true, ack_sla=15s)
    API->>Redis: PUBLISH prism:events:session:{sable_session_id}
    Redis->>WS1: Signal frame delivered to mini1

    Note over WS1,Sable: Doorbell — not payload
    WS1->>Bridge: signal for identity=Sable
    Bridge->>Bridge: Filter system types, check coalescing
    Bridge->>HTTP: Push doorbell (one bell per drain cycle)
    Bridge->>API: Report wake: path=sse, result=rang
    API->>Oblig: Update wake_attempt_count, last_wake_path=sse, last_wake_result=rang
    HTTP-->>Cursor: SSE event: notifications/resources/updated
    Cursor->>Sable: "Resource changed — pending signals"

    Note over Sable,API: Sable drains and acts
    Sable->>HTTP: POST /mcp (prism_signal_ack, trace_id)
    HTTP->>API: Forward ACK
    API->>Oblig: Record model_acted_ack_at
    Sable->>HTTP: POST /mcp (prism_signals_pending)
    HTTP->>API: Forward drain request
    API->>Oblig: Record drained_at
    API->>Sable: Donna's Question envelope
    Bridge->>Bridge: resetCoalescing() — next bell can ring

    Note over Sable,Donna: Sable replies
    Sable->>HTTP: POST /mcp (prism_signal, to=Donna, in_reply_to=signal_id)
    HTTP->>API: Forward reply signal
    API->>Oblig: Record reply_signal_id, terminal_state=completed
```

### Signal Delivery Flow — TaskAssigned Signal (Donna → Sable)

```mermaid theme={null}
sequenceDiagram
    participant Donna as Donna<br/>(Claude Code / mini3)
    participant MCP3 as MCP Server<br/>(mcp-node / mini3)
    participant API as Prism Backend<br/>(server1.home.lan)
    participant Oblig as Signal Obligations<br/>(SPEC-088)
    participant Redis as Redis Pub/Sub
    participant WS1 as WS Client<br/>(stream.ts / mini1)
    participant Bridge as Cursor SSE Bridge<br/>(mini1)
    participant HTTP as HTTP/SSE Server<br/>(mini1)
    participant Sable as Sable<br/>(Cursor Agent)

    Note over Donna,Sable: Donna assigns task to Sable
    Donna->>MCP3: prism_signal(to="Sable", type="TaskAssigned")
    MCP3->>API: POST /api/v1/signal
    API->>Oblig: Create obligation (action_required=true, reply_required=true, terminal_sla=600s)
    API->>Redis: PUBLISH to Sable's session channel

    Redis->>WS1: Signal frame
    WS1->>Bridge: signal for identity=Sable
    Bridge->>Bridge: Not system type, no pending bell → ring
    Bridge->>HTTP: Push doorbell
    Bridge->>API: wake: path=sse, result=rang
    API->>Oblig: Update wake fields
    HTTP-->>Sable: SSE: notifications/resources/updated

    Sable->>API: prism_signal_ack(trace_id)
    API->>Oblig: model_acted_ack_at recorded (within 15s SLA)
    Sable->>API: prism_signals_pending()
    API->>Oblig: drained_at recorded
    API->>Sable: TaskAssigned envelope
    Bridge->>Bridge: resetCoalescing()

    Note over Sable,Donna: Sable completes task and replies
    Sable->>API: prism_signal(to=Donna, type="TaskCompleted", in_reply_to=signal_id)
    API->>Oblig: reply_signal_id set, terminal_state=completed
    API->>Redis: PUBLISH to Donna's session channel
    Redis->>Donna: ChannelsPushStrategy (native Claude Code push)
```

### Signal Delivery Flow — StatusUpdate Signal (Donna → Sable)

```mermaid theme={null}
sequenceDiagram
    participant Donna as Donna<br/>(Claude Code / mini3)
    participant API as Prism Backend<br/>(server1.home.lan)
    participant Oblig as Signal Obligations<br/>(SPEC-088)
    participant Redis as Redis Pub/Sub
    participant WS1 as WS Client<br/>(stream.ts / mini1)
    participant Bridge as Cursor SSE Bridge<br/>(mini1)
    participant HTTP as HTTP/SSE Server<br/>(mini1)
    participant Sable as Sable<br/>(Cursor Agent)

    Note over Donna,Sable: Donna sends status update to Sable
    Donna->>API: prism_signal(to="Sable", type="StatusUpdate")
    API->>Oblig: Create obligation (action_required=false, reply_required=false)
    API->>Redis: PUBLISH to Sable's session channel

    Redis->>WS1: Signal frame
    WS1->>Bridge: signal for identity=Sable
    Bridge->>Bridge: Not system type, no pending bell → ring
    Bridge->>HTTP: Push doorbell
    Bridge->>API: wake: path=sse, result=rang
    API->>Oblig: Update wake fields
    HTTP-->>Sable: SSE: notifications/resources/updated

    Sable->>API: prism_signal_ack(trace_id)
    API->>Oblig: model_acted_ack_at recorded
    Sable->>API: prism_signals_pending()
    API->>Oblig: drained_at, terminal_state=completed (no reply required)
    API->>Sable: StatusUpdate envelope
    Bridge->>Bridge: resetCoalescing()

    Note over Sable: No reply needed — obligation auto-completes on drain
```

### Signal Delivery Flow — System Broadcast (PeerJoined/PeerLeft)

```mermaid theme={null}
sequenceDiagram
    participant System as System<br/>(Prism Backend)
    participant Redis as Redis Pub/Sub
    participant WS1 as WS Client<br/>(stream.ts / mini1)
    participant Bridge as Cursor SSE Bridge<br/>(mini1)
    participant Cache as Signal Cache<br/>(mini1)
    participant Sable as Sable<br/>(Cursor Agent)

    Note over System,Sable: Agent joins — system broadcast
    System->>Redis: PeerJoined (to_identity=*, from=system)
    Redis->>WS1: Signal frame

    WS1->>Bridge: signal type=PeerJoined
    Bridge->>Bridge: SYSTEM_TYPES.has("PeerJoined") → true
    Bridge->>Bridge: FILTERED — no doorbell ring
    Note over Bridge: System signals never ring the bell

    WS1->>Cache: Buffer in signal cache
    Note over Cache: Available on next prism_signals_pending drain

    Note over Sable: Sable sees PeerJoined only on next<br/>scheduled drain or maintenance tick
```

### Signal Delivery Flow — Coalesced Burst (Donna → Sable, multiple signals)

```mermaid theme={null}
sequenceDiagram
    participant Donna as Donna<br/>(Claude Code / mini3)
    participant API as Prism Backend
    participant Redis as Redis Pub/Sub
    participant WS1 as WS Client<br/>(stream.ts / mini1)
    participant Bridge as Cursor SSE Bridge<br/>(mini1)
    participant HTTP as HTTP/SSE Server<br/>(mini1)
    participant Sable as Sable<br/>(Cursor Agent)

    Note over Donna,Sable: Donna sends 3 signals in rapid succession
    Donna->>API: prism_signal(to="Sable", type="Question", ask="review spec")
    Donna->>API: prism_signal(to="Sable", type="StatusUpdate", body="FYI...")
    Donna->>API: prism_signal(to="Sable", type="Question", ask="also check tests")

    API->>Redis: PUBLISH signal 1
    API->>Redis: PUBLISH signal 2
    API->>Redis: PUBLISH signal 3

    Redis->>WS1: Frame 1 (Question)
    WS1->>Bridge: signal 1
    Bridge->>Bridge: No pending bell → RING
    Bridge->>HTTP: Push doorbell
    HTTP-->>Sable: SSE: notifications/resources/updated

    Redis->>WS1: Frame 2 (StatusUpdate)
    WS1->>Bridge: signal 2
    Bridge->>Bridge: Bell already pending → COALESCED
    Note over Bridge: No SSE event — one bell per drain cycle

    Redis->>WS1: Frame 3 (Question)
    WS1->>Bridge: signal 3
    Bridge->>Bridge: Bell still pending → COALESCED

    Note over Sable: Sable responds to the single doorbell
    Sable->>API: prism_signal_ack(trace_id_1)
    Sable->>API: prism_signals_pending()
    API->>Sable: All 3 signals returned in one drain
    Bridge->>Bridge: resetCoalescing() — next bell can ring
```

### Comparison: Stdio vs Streamable HTTP

```mermaid theme={null}
flowchart LR
    subgraph stdio["Stdio (current — surface_support: degraded)"]
        direction TB
        s1["Signal arrives at MCP server"] --> s2["Buffered in memory"]
        s2 --> s3["Waits for next tool call"]
        s3 --> s4["Piggybacked on response"]
        s4 --> s5["wake_path: none<br/>Obligation SLA exempt"]
    end

    subgraph http["Streamable HTTP (proposed — surface_support: full)"]
        direction TB
        h1["Signal arrives at MCP server"] --> h2["Cursor SSE Bridge<br/>(doorbell + coalescing)"]
        h2 --> h3["SSE push to Cursor"]
        h3 --> h4["Model drains immediately"]
        h4 --> h5["wake_path: sse<br/>Obligation SLA tracked"]
    end

    style stdio fill:#7f1d1d,stroke:#991b1b,color:#fff
    style http fill:#14532d,stroke:#166534,color:#fff
```

***

## 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:

| 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"` |

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 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/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)

```json theme={null}
{
  "mcpServers": {
    "prism": {
      "url": "http://127.0.0.1:{port}/mcp",
      "headers": {
        "Authorization": "Bearer {local_token}"
      }
    }
  }
}
```

Versus today's stdio config:

```json theme={null}
{
  "mcpServers": {
    "prism": {
      "command": "node",
      "args": ["C:\\DATA\\Prism\\cli\\dist\\mcp-launcher.js"],
      "env": { ... }
    }
  }
}
```

***

## 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](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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.