Snowflake's MCP Streaming Change Exposes a Client Compatibility Trap

Snowflake's managed MCP server is moving tool-call responses from JSON bodies to SSE streams, forcing custom MCP clients to implement the actual Streamable HTTP contract.

MCP clients that treat remote tool calls like ordinary REST endpoints are about to get a useful reminder: Streamable HTTP is not "POST JSON, read JSON, done."

On August 20, 2026, Snowflake published a pending behavior-change notice for its managed Model Context Protocol server. Starting August 20, 2026, Snowflake says tools/call responses from the managed MCP server return as Server-Sent Events streams instead of single JSON payloads. The change is narrow, but it hits a common implementation shortcut in custom clients, gateways, and internal agent harnesses.

What Happened

Snowflake's notice says its managed MCP server will return tool-call results with Content-Type: text/event-stream instead of Content-Type: application/json. Before the change, a client invoking tools/call against the Snowflake MCP endpoint could read the response as a single JSON document. After the change, the response body is an SSE stream containing one or more data: events, followed by a data: [DONE] event.

The notice is explicit about the affected class of clients: anything that parses tools/call using a one-shot JSON helper such as Python's response JSON parsing or JavaScript fetch(...).then(r => r.json()) will break or behave unpredictably. The client must advertise support for both application/json and text/event-stream in the Accept header, then consume the body as a stream when the server chooses SSE.

Snowflake frames this as alignment with MCP specification version 2025-11-25. The official transport spec for that version defines Streamable HTTP as one of MCP's two standard transports, alongside stdio. For HTTP POST requests carrying JSON-RPC requests, the server may return either one JSON object or an SSE stream. The client must support both cases.

This is not a change to every Snowflake REST API. Snowflake says the behavior applies to the Snowflake-managed MCP server only. It does not apply to external MCP servers configured as connectors on Cortex Agents, and it does not change unrelated Snowflake REST endpoints.

That scope matters. This is a protocol compliance change, not a product-wide migration from JSON APIs to event streams. The lesson for engineers is still broader: MCP-over-HTTP clients need to be protocol clients, not REST wrappers with a familiar-looking JSON-RPC body.

Why It Matters

Most integration bugs around agents are not caused by the model. They come from treating agent infrastructure as thinner than it actually is.

MCP looks simple at the message layer because it uses JSON-RPC. A request to call a tool has a method, an id, a tool name, and arguments. A tool result looks like structured data. That shape tempts teams to build quick internal clients with normal HTTP libraries and synchronous JSON parsing.

That shortcut works until a compliant server exercises a part of the transport contract the client ignored. Snowflake's change is a concrete example. The server can stream the final response. It can emit progress or related messages before the final response. It can close and resume streams under the rules described by the MCP transport. A client that assumes "the HTTP response body is the result object" is not implementing the protocol boundary.

For AI engineering teams, this shows up in four places.

First, data platforms are becoming MCP servers. Snowflake's managed MCP server lets agents discover and invoke tools backed by Cortex Agents, Cortex Search, Cortex Analyst, SQL execution, UDFs, and stored procedures. That means MCP clients are no longer only talking to local filesystem helpers or toy weather servers. They are talking to governed enterprise data systems.

Second, tool responses can be large and slow. Snowflake's documentation notes that agent tool responses can include intermediate steps, tool calls, search results, and citations, and that payloads may become large. Streaming is a natural fit for that world. Even if today's response still contains one final result event, the transport must support incremental delivery and eventual richer server-to-client behavior.

Third, remote MCP crosses real network infrastructure. Proxies, timeouts, load balancers, identity providers, and gateways sit between clients and servers. SSE handling affects retry behavior, cancellation semantics, observability, and error reporting. If your internal agent platform terminates or transforms streams incorrectly, the bug may appear as a model failure even though the issue is transport plumbing.

Fourth, conformance is becoming an operational requirement. Snowflake is not asking clients to adopt a vendor-only extension. It is enforcing a path already present in the MCP spec version Snowflake says it supports. The compatibility burden falls on clients that implemented only the easy subset.

How It Works

The relevant MCP transport is Streamable HTTP. A client sends a JSON-RPC message in an HTTP POST body to a single MCP endpoint. For a request, the server can answer in one of two ways: a regular JSON response or an SSE response stream.

The client declares that it can handle both:

Accept: application/json, text/event-stream
Content-Type: application/json

A simplified tools/call request still looks ordinary:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "business_data_agent",
    "arguments": {
      "message": "Which regions had the largest revenue variance last week?"
    }
  }
}

The difference is in response handling. If the server returns application/json, the client can parse one JSON-RPC response object. If the server returns text/event-stream, the client must parse SSE frames, inspect each data: payload, and keep reading until the stream terminates or reaches the server's completion convention.

For Snowflake's announced behavior, the stream can contain a result event and then [DONE]:

data: { "jsonrpc": "2.0", "id": 1, "result": { ... } }

data: [DONE]

Do not design the parser as if there will always be exactly two events. The MCP transport allows the server to send messages before the final response, and disconnection is not the same as cancellation. A robust client needs a small state machine:

  1. Send POST with the correct Accept header.
  2. Branch on the response Content-Type.
  3. For JSON, parse the response object and match the JSON-RPC id.
  4. For SSE, parse event frames incrementally.
  5. Ignore keepalive or empty frames.
  6. Parse JSON data: frames as JSON-RPC messages.
  7. Correlate the final response to the originating request id.
  8. Treat [DONE] as stream completion if the server uses that sentinel.
  9. Surface protocol errors distinctly from tool-result errors.
  10. Implement explicit cancellation rather than assuming a dropped TCP connection cancelled the tool.

Snowflake's managed MCP server documentation adds the data-platform context. A managed MCP server is created with CREATE MCP SERVER and exposes Snowflake-native objects as MCP tools. Snowflake currently documents tool types for Cortex Agent runs, Cortex Search queries, Cortex Analyst messages, SQL execution, and generic UDF or stored procedure calls. Access is governed through Snowflake privileges: USAGE on the MCP server to connect and discover tools, plus the relevant privileges on the underlying objects to invoke them.

That split between transport and authorization is important. Supporting SSE does not weaken governance. The same tool invocation still runs through Snowflake's MCP server and the configured Snowflake access controls. Streaming changes how the bytes arrive, not who is allowed to ask for them.

Example

Here is the kind of client code that is now too fragile:

const response = await fetch(mcpServerUrl, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: 1,
    method: "tools/call",
    params
  })
});

const message = await response.json();
return message.result;

This code has three problems. It does not advertise text/event-stream. It does not inspect Content-Type. It assumes the HTTP response maps directly to a complete JSON-RPC response.

A better shape is transport-aware:

const response = await fetch(mcpServerUrl, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
    Accept: "application/json, text/event-stream"
  },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: requestId,
    method: "tools/call",
    params
  })
});

const contentType = response.headers.get("content-type") ?? "";

if (contentType.includes("application/json")) {
  const message = await response.json();
  return unwrapJsonRpcResult(message, requestId);
}

if (contentType.includes("text/event-stream")) {
  return readSseJsonRpcResult(response.body, requestId);
}

throw new Error(`Unsupported MCP response type: ${contentType}`);

The real work sits inside readSseJsonRpcResult. That function should parse SSE framing correctly instead of splitting naively on every newline. It should tolerate chunk boundaries in the middle of a frame. It should distinguish a JSON-RPC error response from a tool result where isError is true. It should record intermediate messages for traces, but return only the final correlated result to the caller.

In Python, the same rule applies. Use an SSE-aware client library or implement the event parser deliberately. The fix is not to wrap response.text in string surgery after the fact. That will fail on partial chunks, multi-line data fields, retry fields, event ids, and future messages that are legal under the transport.

This is also a good time to add conformance tests. A custom MCP client should have fixtures for both response modes:

POST tools/call -> application/json -> JSON-RPC result
POST tools/call -> text/event-stream -> JSON-RPC result event -> done
POST tools/call -> text/event-stream -> progress event -> JSON-RPC result
POST tools/call -> text/event-stream -> JSON-RPC error
POST tools/call -> unsupported content type -> transport error

Those tests are not Snowflake-specific. They are the minimum bar for any remote MCP client that claims to support the 2025-11-25 Streamable HTTP transport.

My Take

This is exactly the kind of compatibility break the MCP ecosystem needs more of, not less.

That sounds harsh, but protocol ecosystems mature when major implementations stop preserving accidental behavior forever. Returning a single JSON body made Snowflake's managed MCP server easier for simple clients to consume. It also allowed non-compliant clients to survive in production. Moving tools/call to SSE forces those clients to implement the actual contract.

The deeper issue is that many agent stacks are still built as demos with production credentials. A developer wires an MCP endpoint into a framework, sees a tool result, and ships the integration behind an internal assistant. The first version handles happy-path JSON and maybe a 401. It rarely handles stream resumption, cancellation, progress messages, JSON-RPC error envelopes, tool-level errors, retries, or observability across long-running calls.

That gap is survivable when the MCP server controls a local toy tool. It is not acceptable when the server fronts a warehouse, governed semantic layer, SQL execution tool, or cross-application agent. Snowflake's MCP surface is an enterprise data surface. If your client cannot parse the transport correctly, you cannot reason about reliability, auditability, or failure modes correctly.

I would treat this notice as a prompt to audit every MCP integration, not only Snowflake. Look for code paths that call response.json() directly after a remote MCP POST. Look for API gateways that buffer SSE and strip headers. Look for observability middleware that logs only the final body and loses intermediate events. Look for retry wrappers that resend non-idempotent tool calls after stream errors. Look for clients that do not attach the negotiated protocol version or session headers where their target spec requires them.

The design principle is straightforward: keep the MCP transport layer separate from tool semantics. The transport layer should handle content negotiation, SSE parsing, JSON-RPC correlation, cancellation, retries, and stream lifecycle. The tool layer should handle schemas, authorization outcomes, domain errors, and result interpretation. Blending those concerns into one "call tool" helper is how REST-shaped assumptions leak into an agent protocol.

For teams building internal agent platforms, the cheapest path is usually to use a maintained MCP SDK where possible. If you cannot, build a small conformance harness and run it against every client release. MCP will continue to evolve, and enterprise servers will exercise more of the spec over time.

Conclusion

Snowflake's August 20, 2026 MCP behavior-change notice is not just a minor response-format update. It is a useful boundary test for the agent infrastructure many teams are putting around production systems.

If your client already implements MCP Streamable HTTP correctly, the change should be uneventful. If your client treats MCP as JSON-RPC over ordinary REST, this is the bug report arriving before the outage.

The fix is clear: send Accept: application/json, text/event-stream, branch on response content type, parse SSE as a stream, correlate JSON-RPC messages by id, and test both response modes. Once MCP servers front real data platforms, that work is not polish. It is table stakes.

Sources

Enjoying this article?

Subscribe to get new NestJS, Node.js, and backend engineering posts directly in your inbox.

Subscribe for free