Kuberwastaken/claurst · error

MCP server ' ': failed to parse legacy SSE JSON payload

Error message

MCP server '{}': failed to parse legacy SSE JSON payload: {}

What it means

Raised by `parse_server_message` when a JSON payload arriving from a legacy SSE MCP server (either from the long-lived SSE stream or from a POST response body) cannot be deserialized into an rmcp `RxJsonRpcMessage<RoleClient>`. The library expects every server-sent event data block to be a valid JSON-RPC 2.0 message conforming to the MCP schema. If the payload is malformed JSON, empty-but-not-blank, or structurally not a JSON-RPC message, serde deserialization fails and the server name plus serde error are wrapped into this anyhow error.

Solutions

  1. Inspect the raw SSE payload (log `data` before parsing) to see what the server actually sent.
  2. Confirm the endpoint is a real MCP server speaking the SSE transport (GET /sse), not a generic HTTP route.
  3. Upgrade the MCP server to a protocol version compatible with the rmcp client schema.
  4. If the server sends non-JSON keepalive frames, configure it to use SSE comments (`: ping`) instead of data events.

Example fix

// before: pointing the SSE transport at the messages endpoint instead of the SSE endpoint
let backend = connect_legacy_sse("http://host:3000/message").await?;
// after: use the server's advertised SSE endpoint
let backend = connect_legacy_sse("http://host:3000/sse").await?;
Defensive patterns

Strategy: validation

Validate before calling

fn looks_like_jsonrpc(data: &str) -> bool {
    serde_json::from_str::<serde_json::Value>(data)
        .map(|v| v.get("jsonrpc").is_some() && v.get("method").is_some() || v.get("id").is_some())
        .unwrap_or(false)
}

Type guard

fn as_jsonrpc_message(data: &str) -> Option<serde_json::Value> {
    serde_json::from_str(data).ok().filter(|v| v.is_object())
}

Try / catch

match parse_server_message(&server_name, data) {
    Ok(msg) => incoming_tx.send(msg).ok(),
    Err(e) => {
        tracing::warn!(%server_name, raw = %data, "dropping unparseable SSE payload: {e}");
        Ok(())
    }
}

Prevention

When it happens

Trigger: `parse_server_message` called from `start_sse_listener` or `handle_legacy_sse_http_response` with `data` that is not valid JSON-RPC: server sends HTML error pages over SSE, sends JSON that lacks required JSON-RPC fields (e.g. no `jsonrpc: "2.0"`), sends a JSON array, or truncates a message.

Common situations: Connecting the legacy SSE transport to a non-MCP HTTP endpoint that streams HTML or plain text; an old MCP server implementing a pre-standard protocol revision whose message shapes no longer deserialize; a proxy mangling the SSE stream; server bug emitting comments/keepalives as data payloads.

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10). Data as JSON: /api/errors/efc0422bda0187e2. Report an issue: GitHub.

Appendix: source

Thrown at src-rust/crates/mcp/src/rmcp_backend.rs:428

    fn close(&mut self) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send {
        let background_tasks = Arc::clone(&self.background_tasks);
        async move {
            let mut tasks = lock_recover(&background_tasks);
            for handle in tasks.drain(..) {
                handle.abort();
            }
            Ok(())
        }
    }
}

fn parse_server_message(
    server_name: &str,
    data: &str,
) -> anyhow::Result<rmcp::service::RxJsonRpcMessage<RoleClient>> {
    serde_json::from_str(data).map_err(|e| {
        anyhow::anyhow!(
            "MCP server '{}': failed to parse legacy SSE JSON payload: {}",
            server_name,
            e
        )
    })
}

async fn handle_legacy_sse_http_response(
    server_name: String,
    response: reqwest::Response,
    incoming_tx: mpsc::UnboundedSender<rmcp::service::RxJsonRpcMessage<RoleClient>>,
    background_tasks: Arc<StdMutex<Vec<JoinHandle<()>>>>,
) -> anyhow::Result<()> {
    let status = response.status();
    if !status.is_success() && status != reqwest::StatusCode::ACCEPTED {
        let body = response.text().await.unwrap_or_default();
        anyhow::bail!(
            "MCP server '{}': HTTP {} from legacy SSE transport: {}",

View on GitHub (pinned to b0637c97ec)