Skip to main content

Module mcp_notify

Module mcp_notify 

Source
Expand description

v0.11.0 P4 — bridge per-tenant InvalidateEvent broadcasts into MCP notifications/message events on each session’s SSE stream.

§Scope

v0.11.0 ships only the two MCP-spec-defined notification shapes (notifications/progress and notifications/message) per plan §3 Decision B. P3 wired notifications/progress (per-tool progress). P4 wires notifications/message by subscribing to each tenant’s existing TenantHandle::invalidate_sender() broadcast channel (the same channel that powers /v1/graph/stream for solo-web) and mapping each InvalidateEvent into a JSON-RPC notifications/message envelope on the per-session broadcast channel from P1+P2.

§Mapping policy (locked for v0.11.0)

InvalidateEvent carries a kind discriminator (episode, document, chunk, cluster, triple, tenant) and a reason string (memory.remember, memory.forget, …) per docs/dev-log/0118-graph-stream-impl.md. P4 maps these conservatively into MCP data strings the client can switch on:

kinddata stringNotes
episodememories_updatedCoarse signal: any episode-level change
documentdocuments_updatedIngest / forget at document granularity
chunkdocuments_updatedCoalesced under documents (chunks are a leaf of one document)
clusterconsolidation_updatedSteward consolidation landed
triplegraph_updatedTriples-extract committed
tenanttenant_updatedGDPR cascade or tenant lifecycle
othermemory_updatedFallback so a future writer kind never silently drops

level = "info" for every variant; logger = "solo". The MCP spec’s notifications/message shape allows level ∈ {debug, info, warning, error}; v0.11.0 emits at info because every wire shape is a committed write (invariant: InvalidateEvent is only sent after the writer-actor’s commit succeeds — see solo_core::InvalidateEvent’s docstring).

Each emitted MCP message ALSO carries the original reason, tenant_id, and ts_ms in a nested details object so clients that want the structured shape get it without losing the coarse data switch. The two halves stay in lock-step — data is what to refetch; details.reason is why.

§Per-session task lifecycle

For each session created by POST /mcp, the POST handler calls spawn_invalidate_bridge passing the freshly-resolved Arc<TenantHandle> + Arc<SessionState>. The function:

  1. Calls tenant.invalidate_sender().subscribe() BEFORE downgrading the session Arc to a Weak<SessionState>.
  2. Spawns a tokio::spawn task that loops on rx.recv():
    • Ok(event) → map via map_invalidate_to_message, call session.publish_event(Message, envelope) via the Weak upgrade. Returns immediately if upgrade fails (session dropped — task exits).
    • Err(RecvError::Lagged(n)) → log warn (matches /v1/graph/stream discipline) and continue. The session’s own event-channel lag handling kicks in on the GET side; the bridge itself is best-effort fan-out.
    • Err(RecvError::Closed) → tenant handle dropped (rare outside test shutdown); task exits cleanly.

The task holds NO strong reference to SessionState — only a Weak. The session’s Arc lives in the [SessionStore]; when the store drops the session (TTL expiry or delete), the Weak upgrade fails on the next event and the task exits. This is the session_task_exits_when_session_dropped test contract.

§Out of scope (v0.12.0+)

  • Coalescing rapid bursts. v0.11.0 emits one MCP message per InvalidateEvent. A pathological pattern (e.g. 1000 rapid memory.remember calls under one session) would emit 1000 messages. The per-session broadcast channel is bounded (crate::mcp_session::MCP_SESSION_EVENT_BUFFER_CAPACITY = 256), so a slow GET subscriber would see event: lagged per P2 rather than memory growth. A future v0.12.0 follow-up could coalesce bursts at the bridge tier (e.g. “if N events of the same kind in 1s, emit one”); not needed for v0.11.0 launch.
  • Solo-custom event types (audit_event, memory_consolidated, tenant_changed as Solo-custom shapes). Deferred to v0.12.0+ per plan §3 Decision B. P4 ships only spec-compliant notifications/message.
  • Per-event auth scoping. The bridge inherits the session’s bound tenant; cross-tenant filtering happens at the broadcast layer (each tenant has its own invalidate_sender) so cross- tenant leakage is impossible by construction.

Constants§

MCP_NOTIFICATION_DATA_CONSOLIDATION_UPDATED
data discriminator for consolidation/cluster invalidations.
MCP_NOTIFICATION_DATA_DOCUMENTS_UPDATED
data discriminator for document-level invalidations. Includes chunks (coalesced — chunks are a leaf of a parent document, so refetching the document covers both).
MCP_NOTIFICATION_DATA_GRAPH_UPDATED
data discriminator for graph (triples) invalidations.
MCP_NOTIFICATION_DATA_MEMORIES_UPDATED
data discriminator for episode-level invalidations. Coarse signal: “any episode-level memory changed; refetch what you care about”. Maps InvalidateEvent.kind = "episode".
MCP_NOTIFICATION_DATA_MEMORY_UPDATED
Fallback data for InvalidateEvent.kind values P4 doesn’t recognise. Defensive: a future writer command emits a new kind string we haven’t mapped yet → the client still sees a refetch signal rather than the bridge dropping the event silently.
MCP_NOTIFICATION_DATA_TENANT_UPDATED
data discriminator for tenant-lifecycle invalidations (today fired by the GDPR forget_user cascade per solo_core::InvalidateEvent’s kind taxonomy).
MCP_NOTIFICATION_MESSAGE_LEVEL
level field of every MCP notifications/message envelope this bridge emits. The spec permits debug / info / warning / error; v0.11.0 emits info because the invariant on solo_core::InvalidateEvent is that it ONLY fires after a successful writer-actor commit — there is no error/warning path.
MCP_NOTIFICATION_MESSAGE_LOGGER
logger field of every MCP notifications/message envelope this bridge emits. The MCP spec requires logger to be a server-chosen identifier — "solo" distinguishes our messages from any proxy / middleware that might emit on the same stream.
MCP_NOTIFICATION_MESSAGE_METHOD
JSON-RPC method literal for MCP notifications/message. Held as pub const so audits can grep the wire literal once and trust it never drifts (Lesson #25 / #39 grep-ability).

Functions§

map_invalidate_to_message
Map one solo_core::InvalidateEvent into the MCP spec’s notifications/message JSON envelope.
spawn_invalidate_bridge
Spawn the per-session invalidate-bridge task.