Hmbown/CodeWhale · error

MCP SSE server sent a message before declaring its endpoint

Error message

MCP SSE server sent a message before declaring its endpoint

What it means

Per the SSE transport protocol, the server must send its endpoint event before any JSON-RPC messages. If a `Message` arrives on the inbound channel before an `Endpoint`, the client rejects it as a protocol violation rather than buffering or guessing the endpoint.

Solutions

  1. Fix the server to send the `endpoint` event as the first SSE event after connection
  2. If testing, update the mock to emit `event: endpoint` before any message events
  3. Check whether a stale session/stream replay is occurring and open a fresh connection
  4. Verify server MCP SDK version supports the SSE transport handshake correctly

Example fix

// server, before
send(data_event);
send(endpoint_event);
// after
send(endpoint_event); // endpoint MUST come first
send(data_event);
Defensive patterns

Strategy: try-catch

Try / catch

match wait_for_endpoint(...).await {
    Err(e) if e.to_string().contains("before declaring its endpoint") => {
        error!("server violates SSE handshake order; update server or SDK");
    }
    other => other?,
}

Prevention

When it happens

Trigger: Server emits JSON-RPC messages before the endpoint event — a non-conformant SSE MCP server implementation or a stream resumed mid-session replaying old messages.

Common situations: Custom/in-house MCP SSE servers that send data immediately; resuming a cached stream that replays messages; testing against a mock that skips the endpoint event.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/2a8edb1034650d34. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/mcp/sse.rs:231

        let msg = tokio::select! {
            _ = cancel_token.cancelled() => {
                anyhow::bail!("SSE transport cancelled before endpoint was discovered");
            }
            _ = &mut timeout => {
                anyhow::bail!(
                    "SSE endpoint not received within {}ms",
                    endpoint_timeout.as_millis()
                );
            }
            msg = self.receiver.recv() => {
                msg.context("SSE transport closed before endpoint was discovered")?
            }
        };

        match msg {
            SseInbound::Endpoint(endpoint) => self.store_endpoint(&endpoint),
            SseInbound::Message(_) => {
                anyhow::bail!("MCP SSE server sent a message before declaring its endpoint");
            }
        }
    }

    fn store_endpoint(&mut self, endpoint: &str) -> Result<()> {
        self.endpoint_url = Some(Self::resolve_endpoint_url(&self.base_url, endpoint)?);
        Ok(())
    }

    fn resolve_endpoint_url(base_url: &str, endpoint_url: &str) -> Result<String> {
        let base = reqwest::Url::parse(base_url)?;
        let resolved =
            if endpoint_url.starts_with("http://") || endpoint_url.starts_with("https://") {
                reqwest::Url::parse(endpoint_url)?
            } else {
                base.join(endpoint_url)?
            };
        // Security: the server-supplied `endpoint` event must stay same-origin

View on GitHub (pinned to 73e0f67d83)