MCP Exchanges

Kinds 0 to 4: the five MCP exchanges the proxy asks; the server answers four of them with exactly one message each, and the fifth with one message per notification for the life of its path.

Five asks carry the MCP exchanges of the container. The server answers four of them with exactly one message each. It answers the fifth with one message per notification for as long as the path lives.

KindAskPayloadAnswered onAnswer
0list toolsthe params as JSON/mcp/list-tools/{channel}exactly one message
1list resourcesthe params as JSON/mcp/list-resources/{channel}exactly one message
2call toolthe params as JSON/mcp/call-tool/{channel}exactly one message
3read resourcethe params as JSON/mcp/read-resource/{channel}exactly one message
4notificationsnothing/mcp/notifications/{channel}one message per notification
  • The payloads. A params payload is the JSON of the request type its page shows, read as Notation provides. The types named from rmcp::model are those of the Model Context Protocol, incorporated by reference.
  • The answer. For kinds 0 to 3, the server sends exactly one message on the path — the response frame of the exchange, carrying a result or an error — and closes the connection. For kind 4, the server sends one message per notification for as long as the path lives; when it sends an error, the error is the last message; the server closes the connection when it has no more to send.
  • When an exchange is answered. An exchange is answered when its message has arrived and its path has closed cleanly. An exchange whose /requests connection ended before the server opened its path, and an exchange whose path ended abruptly, are not answered; the proxy asks such an exchange again on the next /requests connection, under a fresh channel. A server may therefore receive one exchange more than once. A path that the proxy or the server closes cleanly with no message before the close states that the ask was not served. The proxy does not ask an unserved exchange again.
  • The notification stream. The proxy asks for notifications only after it has made its first exchange of kinds 0 to 3. When a notification path ends, cleanly, by an error, or abruptly, the proxy asks for notifications again on the next /requests connection. The server does not replay a notification that occurred while no notification path was open.

The ways in which a response fails to decode are shared by the five exchanges and are defined by diverge-provider-sdk/src/shared/mcp/frame_error.rs:

//! An MCP answer that could not be read.

use std::error;
use std::fmt;

/// What went wrong reading one of the five answers.
///
/// # One type for five frames
///
/// Which is unusual here: this crate writes an error per frame, so that
/// what can go wrong reading one is spelled out beside it.
///
/// These five have nothing to spell out separately. Every one is a tag
/// and then JSON, so every one fails in the same three ways, and five
/// copies would differ only in the module they sat in — while every
/// consumer that handles more than one of them would need a variant per
/// copy to say the same thing.
///
/// A caller reading a tool listing already knows it was reading a tool
/// listing. The error does not have to tell it.
#[derive(Debug)]
pub enum FrameError {
    /// No bytes at all, so not even a tag.
    Empty,
    /// A tag that is neither of the frame's two.
    ///
    /// What a caller newer than this one produces, which is the case a
    /// tag exists to make survivable: a reader that does not know a
    /// variant says so, rather than reading somebody else's bytes as
    /// its own.
    UnknownTag(u8),
    /// The payload after the tag did not parse.
    Body(serde_json::Error),
}

impl fmt::Display for FrameError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FrameError::Empty => f.write_str("mcp answer frame is empty"),
            FrameError::UnknownTag(tag) => {
                write!(f, "unknown mcp answer frame tag {tag}")
            }
            FrameError::Body(error) => {
                write!(f, "mcp answer did not parse: {error}")
            }
        }
    }
}

impl error::Error for FrameError {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match self {
            FrameError::Body(error) => Some(error),
            FrameError::Empty | FrameError::UnknownTag(_) => None,
        }
    }
}