/mcp/notifications/{channel}
Kind 4: the notifications the caller’s servers send on their own account; the server sends one message per notification for the life of the path, an error as the last message when there is one, and closes the connection.
The ask is kind 4 on /requests. Its payload
is empty. The server answers on /mcp/notifications/{channel} by
sending one message per notification for as long as the path lives.
Each message is the response frame defined below: its first byte is
0, followed by the notification as JSON. A message whose first byte
is 1, followed by an error as JSON, is the last message the server
sends. The server closes the connection when it has no more to send.
A path that the proxy or the server closes cleanly with no message
before the close states that the ask was not served.
- When the proxy asks. The proxy asks for notifications after
its first exchange of kinds
0to3, and it keeps one such ask open for the rest of its life. - A stream that ended. When a notification path ends, cleanly,
by an error, or abruptly, the proxy asks for notifications again on
the next
/requestsconnection. The server does not replay a notification that occurred while no notification path was open.
The ask is defined by diverge-provider-sdk/src/shared/mcp/notifications/request/request.rs:
//! Asking to hear what a server says.
use std::convert::Infallible;
use crate::decode::Decode;
use crate::encode::{Encode, Writer};
/// Open the notification stream.
///
/// It carries nothing, and MCP is the reason rather than an economy.
/// Every other exchange here is a JSON-RPC method, and a request naming
/// one carries its params. This is not a method: in Streamable HTTP a
/// client opens the notification stream with a bare `GET` on the same
/// url it POSTs everything else to — no method name, no body, nothing
/// to say. There is no `notifications/subscribe` to mirror.
///
/// So the empty payload is the request, whole.
///
/// # It exists anyway
///
/// Rather than the channel request simply carrying no payload for this
/// one case. A frame that names five things should name them the same
/// way, and a variant with nothing in it is a variant a reader has to
/// check twice — once for what it means, once for why it is shaped
/// differently.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Request;
/// Nothing at all. Which channel it arrives on says what it is, and
/// there is nothing else to say.
impl Encode for Request {
/// [`Infallible`]: no bytes.
type Error = Infallible;
fn encode(&self, _out: &mut Writer<'_>) -> Result<(), Infallible> {
Ok(())
}
}
impl Decode<'_> for Request {
/// [`Infallible`]: nothing is read, so nothing can be wrong.
type Error = Infallible;
/// Whatever bytes are there are ignored rather than rejected. There
/// is nothing this could carry, so a reader that found something
/// has met a writer from a version that gave it one — and the
/// channel already said what was meant.
fn decode(_bytes: &[u8]) -> Result<Self, Infallible> {
Ok(Request)
}
}
The messages are defined by diverge-provider-sdk/src/shared/mcp/notifications/response/frame.rs:
//! What arrives on a notification channel.
use rmcp::ErrorData;
use rmcp::model::ServerNotification;
use super::super::super::FrameError;
use crate::decode::Decode;
use crate::encode::{Encode, Writer};
/// One thing the server said on its own account, or the reason it will
/// say nothing further.
///
/// The payload of a
/// [`ClientFrame::ChannelResponse`](crate::frame::client::ClientFrame::ChannelResponse)
/// on a channel opened by a
/// [`request::Request`](super::super::request::Request).
///
/// A payload leads with one byte saying which — `0` for
/// [`Notification`](Self::Notification), `1` for
/// [`Error`](Self::Error) — and the rest is that variant's own JSON.
///
/// # Many frames, unlike the other four
///
/// The other exchanges answer once and finish. This one carries a frame
/// per notification for as long as the channel lives, because that is
/// what it is: not an answer, but the stream a server pushes into when
/// something changes.
///
/// Nothing terminates it in the payload. The channel's own finish says
/// there will be no more, which is what a finish already means and what
/// makes a second signal for one fact a second thing to disagree about.
///
/// # An error is the last thing on it
///
/// A notification stream that stops is a stream that stopped, and a
/// caller that can no longer keep one open says so here rather than
/// finishing in silence. Nothing follows it; the channel finishes
/// after.
///
/// It is [`ErrorData`] rather than
/// [`shared::error::Error`](crate::shared::error::Error) for the reason
/// the other four give: what is being relayed is an MCP server's
/// failure, not the provider's, and a JSON-RPC code is content rather
/// than detail.
///
/// # [`ServerNotification`] is the whole union, deliberately
///
/// Every notification a server can send, including the ones this crate
/// has no opinion about. Narrowing it to the ones an agent obviously
/// acts on — tools changed, resources changed — would mean deciding on
/// an agent's behalf what is worth hearing, and would need revising
/// every time MCP adds one.
///
/// It deserializes by its `method`, which each variant fixes to a
/// constant that rejects every other value. So the union is untagged in
/// serde's terms and discriminated in practice, and a notification this
/// crate's `rmcp` is too old to know arrives as
/// `CustomNotification` rather than as a failure.
///
/// # It does not derive [`PartialEq`]
///
/// Alone among the five, because
/// [`ServerNotification`] does not. Comparing two is not something
/// anything here does, and wrapping the union to add it would be
/// keeping a second copy of `rmcp`'s type for the sake of a derive.
#[derive(Debug, Clone)]
pub enum Frame {
/// One notification, as the server sent it. Tag `0`.
Notification(ServerNotification),
/// The stream ended early, and this is why. Tag `1`.
Error(ErrorData),
}
/// Tag for [`Frame::Notification`].
const NOTIFICATION: u8 = 0;
/// Tag for [`Frame::Error`].
const ERROR: u8 = 1;
impl Encode for Frame {
/// The ordinary JSON failure. Both variants are serialized and the
/// tag cannot fail.
type Error = serde_json::Error;
// Spelled out rather than `Self::Error`: this enum has a variant
// called `Error`, so the associated type is ambiguous by that name.
fn encode(&self, out: &mut Writer<'_>) -> Result<(), serde_json::Error> {
match self {
Frame::Notification(notification) => {
out.extend_from_slice(&[NOTIFICATION]);
serde_json::to_writer(out, notification)
}
Frame::Error(error) => {
out.extend_from_slice(&[ERROR]);
serde_json::to_writer(out, error)
}
}
}
}
impl Decode<'_> for Frame {
/// Three ways to fail, and only one of them is JSON.
type Error = FrameError;
// Spelled out for the same reason as `encode` above.
fn decode(bytes: &[u8]) -> Result<Self, FrameError> {
let (tag, rest) = bytes.split_first().ok_or(FrameError::Empty)?;
match *tag {
NOTIFICATION => serde_json::from_slice(rest)
.map(Frame::Notification)
.map_err(FrameError::Body),
ERROR => serde_json::from_slice(rest)
.map(Frame::Error)
.map_err(FrameError::Body),
tag => Err(FrameError::UnknownTag(tag)),
}
}
}