← Back to list

Choosing the Wrong MCP Transport Will Break Your Agent in Production — Here’s the Decision

stdio, SSE, and Streamable HTTP aren’t interchangeable — here’s what each one actually costs you

PATEL VISHADKUMAR TULSIDAS in Stackademic · 2026-06-02 11:31 · 1 claps · 14.3 min read paywalled
#mcp-server #spring-ai #streamable-http #server-sent-events #java
Open on Medium ↗
Wiki topics: AGT · AI Agents

Choosing the Wrong MCP Transport Will Break Your Agent in Production — Here’s the Decision

stdio, SSE, and Streamable HTTP aren’t interchangeable — here’s what each one actually costs you

The Production MCP Notebook · Article 3 · Beginner Track

MCP defines three transports — stdio for local subprocess communication, the deprecated HTTP+SSE for early remote servers, and Streamable HTTP as the modern remote standard. The decision between them is not a matter of preference. stdio servers cannot serve multiple users at once. SSE servers will stop working as removal deadlines arrive in mid-2026, with Atlassian’s Rovo MCP server scheduled for 30 June 2026 and others following. Streamable HTTP is the only remote transport with a future. The transport decision determines whether your server can be deployed at all, whether it survives the deprecation window, and whether it scales beyond a single user. Most teams pick wrong twice before getting it right. This article is about making the decision once.

***🌟 Open Access Version ✨ Knowledge wants to be free. 📚 Enjoy the full read — free & unrestricted***

The Problem

A team I worked with shipped their first MCP server in early 2025. They built it on the HTTP+SSE transport because that’s what the tutorial they followed used, and it worked fine in their proof of concept. Six months later they were running into three problems simultaneously.

First, ChatGPT support landed and their server didn’t work with it. ChatGPT only speaks Streamable HTTP. Their SSE server was invisible to one of the largest agent platforms in production.

Second, their load balancer kept dropping SSE connections at the thirty-second idle timeout. Long-running tool calls failed with cryptic errors. Their ops team spent two weeks tracing it before realizing the transport itself was the problem — SSE’s separate-channel design (one endpoint for the event stream, another for client-to-server messages) didn’t survive their infrastructure’s standard timeout configuration.

Third, Atlassian announced that their Rovo MCP server’s SSE endpoint would be shut off on 30 June 2026. The team’s customers used the Atlassian integration. Their server, which proxied to Atlassian, would now have to migrate too — and they realized they’d be doing the same migration on every upstream service that followed Atlassian’s lead.

The team didn’t have a code problem. They had a transport problem, made eighteen months earlier when they picked SSE because the docs they read at the time recommended it. The transport you choose at the start of an MCP project determines what infrastructure can host it, what clients can reach it, and how long it survives.

This article is about understanding the three transports well enough to make that decision once and not have to revisit it.

What Each Transport Actually Is

The MCP specification separates the protocol layer (JSON-RPC over message exchange) from the transport layer (how those messages physically move between client and server). All three transports carry the same protocol; they differ in how the bytes travel.

stdio

The client launches the server as a subprocess. The client writes JSON-RPC messages to the server’s stdin. The server writes responses to stdout. Each message is one line of JSON, delimited by a newline. The server can write logs to stderr, which the client may or may not display but never confuses with protocol messages.

That’s the entire transport.

┌──────────┐                ┌──────────┐
│          │   stdin        │          │
│  CLIENT  ├───────────────►│  SERVER  │
│          │                │          │
│          │   stdout       │          │
│          │◄───────────────┤          │
│          │                │          │
│          │   stderr (logs)│          │
│          │◄───────────────┤          │
└──────────┘                └──────────┘
                            (subprocess)

The host runs your server as a child process. Communication is point-to-point. There is no network. No authentication. No multi-tenancy. When the client exits, the subprocess dies.

stdio is the right transport when the server runs on the same machine as the client, with the same trust boundary as the user, and serves exactly one client. Claude Desktop launching a local filesystem server is the canonical example. The Playwright MCP server is another — it controls a browser on the local machine and there is no scenario where someone else’s browser should be controllable through it.

The strength of stdio is exactly its limitation. There is nothing to misconfigure because there is almost nothing to configure. The weakness is that nothing about it is shareable. A stdio server cannot be a service. It is a tool the host runs on demand.

HTTP+SSE (deprecated)

The original remote transport, introduced in the 2024–11–05 specification. The server exposes two endpoints. One is a GET endpoint that the client connects to and holds open as a Server-Sent Events stream — the server pushes messages to the client over this stream. The other is a POST endpoint where the client sends its requests. The session is identified by a session ID exchanged at connection time.

GET /sse  (held open, server pushes)
   ┌──────────┐ ◄─────────────────────────────────── ┌──────────┐
   │  CLIENT  │                                      │  SERVER  │
   │          │ ──────────────────────────────────►  │          │
   └──────────┘   POST /messages  (client sends)     └──────────┘
                  (session correlated via session ID)

The split-endpoint design is what kills it. The long-lived SSE connection is fragile under any load balancer, proxy, or firewall that enforces idle timeouts — and almost all of them do. Session recovery after a network drop is awkward. Authentication, CORS, and rate limiting have to be coordinated across two endpoints. The pattern was novel enough in November 2024 that the spec accepted it; by March 2025 the working group had concluded it wasn’t a good fit for production and shipped its replacement.

If you are starting a new MCP server today, do not use SSE. The remainder of this section exists only so you understand what you’re looking at when you encounter a legacy server.

Streamable HTTP

The modern remote transport, introduced in the 2025–03–26 specification and refined in 2025–11–25. The server exposes a single endpoint that handles both POST and GET. The client sends JSON-RPC over POST. The server responds with either a single JSON response (for simple request/response interactions) or upgrades to an SSE stream embedded in the same HTTP response (for streaming or long-running operations). GET on the same endpoint is used optionally for server-initiated messages.

POST /mcp  (request)
   ┌──────────┐ ──────────────────────────────────► ┌──────────┐
   │          │                                     │          │
   │  CLIENT  │ ◄────── application/json ────────── │  SERVER  │
   │          │       (single response)             │          │
   │          │                or                   │          │
   │          │ ◄──── text/event-stream ─────────── │          │
   │          │       (multiple messages)           │          │
   └──────────┘                                     └──────────┘
              GET /mcp  (optional — server initiates)

The single-endpoint design is the entire point. One URL. One set of headers. One CORS policy. One authentication filter. The server decides per request whether to respond with plain JSON or upgrade to a stream. Load balancers, reverse proxies, and CDNs treat it as a normal HTTP service because that’s what it is.

Streamable HTTP is the only remote transport you should be building against in 2026. Every major MCP client supports it. ChatGPT supports only Streamable HTTP. The migration deadlines for SSE servers are arriving across the industry. If you are deploying an MCP server over a network, this is the transport.

A Decision Framework That Resolves It in One Question

The decision between transports collapses to a single question: who is going to use this server?

If the answer is “the same human, on the same machine, where the server runs as part of the host’s process tree” — use stdio. Examples: a developer’s local Postgres server with read-only access to their dev database, a filesystem server scoped to the user’s home directory, a Playwright server controlling the user’s own browser.

If the answer is “anyone other than the local user, or more than one user, or the server runs on different infrastructure than the host” — use Streamable HTTP. Examples: a corporate MCP server exposing the company’s CRM, a SaaS-vendor MCP server like Stripe’s or Atlassian’s, an internal platform team’s MCP server consumed by multiple agent applications.

If the answer is “this server already exists and uses SSE” — migrate to Streamable HTTP before the deprecation window closes. Atlassian’s Rovo server is removing SSE on 30 June 2026; other deadlines will follow. The dual-mode pattern (covered later in this article) lets you support both during the cutover.

The first question is rarely as ambiguous as it sounds. Servers that talk to local hardware, local processes, or files on the user’s disk are almost always stdio. Servers that talk to a SaaS API, an internal corporate system, or anything else outside the user’s machine are almost always Streamable HTTP. The rare middle case — a server that needs to access local files but also serve multiple users — is almost always a sign that you’re conflating two different servers into one.

Building a Streamable HTTP Server in Spring AI

A Spring Boot project with the Spring AI MCP server starter for WebMVC. The dependency is different from the stdio starter we used in the first two articles:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

The configuration shifts from stdio’s banner-suppressed, log-silenced setup to a normal Spring Boot web application:

spring:
  ai:
    mcp:
      server:
        name: weather
        version: 1.0.0
        protocol: STREAMABLE
        streamable-http:
          mcp-endpoint: /mcp
server:
  port: 8080

Three things changed from the stdio configuration. The protocol property is now STREAMABLE — by default Spring AI's web starter would use SSE, which is deprecated, so you must set this explicitly. The mcp-endpoint property names the single URL the server exposes. The web application type is no longer disabled, because the whole point of Streamable HTTP is that it runs as a real HTTP service.

Tool registration is identical to the stdio version. Spring AI deliberately keeps the tool API transport-agnostic, so a server you wrote against stdio in development can be redeployed against Streamable HTTP in production without changing tool code:

@Service
public class WeatherTools {
private final WeatherClient client;
    public WeatherTools(WeatherClient client) {
        this.client = client;
    }
    @Tool(
        name = "getCurrentWeather",
        description = """
            Get the current weather for a city. Returns temperature in
            Celsius, conditions (clear, cloudy, rain, snow), and humidity.
            Use this when the user asks about weather right now. Do not
            use this for historical weather or forecasts more than 24
            hours out.
            """
    )
    public CurrentWeather getCurrentWeather(
            @ToolParam(description = "City name, e.g. 'San Francisco' or 'Tokyo'")
            String city) {
        return client.fetchCurrent(city);
    }
}
@Configuration
public class McpConfig {
    @Bean
    public ToolCallbackProvider weatherTools(WeatherTools tools) {
        return MethodToolCallbackProvider.builder()
            .toolObjects(tools)
            .build();
    }
}

Start the application. The MCP endpoint is available at http://localhost:8080/mcp. Verify it with the MCP Inspector or with a raw HTTP call:

curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list"
  }'

The response comes back as application/json for this simple request. For a long-running tool call the same endpoint would respond with text/event-stream and stream messages over the open connection. The client doesn't have to choose in advance which mode it wants; the server picks based on the operation.

Adding OAuth 2.1

Production Streamable HTTP servers must be authenticated. The Spring AI community module mcp-server-security-spring-boot adds OAuth 2.0 resource server support with minimal configuration:

<dependency>
    <groupId>org.springaicommunity</groupId>
    <artifactId>mcp-server-security-spring-boot</artifactId>
    <version>0.1.11</version>
</dependency>

With the JWT issuer URI set in configuration:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com

The auto-configuration registers a SecurityFilterChain that secures the MCP endpoint with bearer token authentication. Unauthorized requests receive an HTTP 401 with a WWW-Authenticate header pointing at the protected resource metadata, which is what MCP-aware clients use to discover where to obtain a token. We come back to OAuth 2.1 specifics — resource indicators, audience binding, the token confusion vulnerabilities — in a dedicated article later in the publication. For now, the relevant fact is that securing a Streamable HTTP server is a normal Spring Security configuration, not a bespoke MCP problem.

The same security module supports API key authentication for internal or machine-to-machine scenarios, which is useful for the first deployment of an internal server before a full OAuth setup is in place.

The Migration Path: Supporting Both Transports During Cutover

If you have an existing SSE server with clients in the field, you cannot switch them all at once. The pattern that works is to run both transports in parallel during a defined window, then remove SSE once your client base has migrated.

The MCP specification supports this explicitly. A server can host both the legacy SSE endpoints and the new Streamable HTTP endpoint simultaneously. Clients that understand Streamable HTTP use it; clients that only understand SSE continue to work against the legacy endpoints.

Spring AI’s webmvc starter does not currently bundle both transports in a single configuration — you set protocol to either SSE or STREAMABLE. The migration pattern in Spring AI is to run two server instances during the cutover window, fronted by a reverse proxy:

                    ┌──────────────────────┐
                    │   Reverse proxy      │
                    │   (nginx, Envoy,     │
                    │    Spring Gateway)   │
                    └─────┬──────────┬─────┘
                          │          │
              /sse, /messages       /mcp
                          │          │
                    ┌─────▼────┐ ┌───▼──────┐
                    │ SSE      │ │ Streamable│
                    │ server   │ │ HTTP      │
                    │ (legacy) │ │ server    │
                    │ protocol:│ │ protocol: │
                    │  SSE     │ │ STREAMABLE│
                    └──────────┘ └───────────┘
                         │            │
                         └────┬───────┘
                              │
                       Same tool code
                       (shared module)

The two server processes share the same tool implementations as a library dependency. Tool registration, business logic, and downstream service connections are identical. Only the transport configuration differs. The proxy routes by path: requests to /sse and /messages go to the legacy server, requests to /mcp go to the new one.

A cleaner alternative if you control the client base: announce a cutover date, run both endpoints, monitor SSE traffic until it drops to zero or near-zero, then decommission the legacy server. Six weeks of overlap is typically enough for an internal client base; external customers may need months. Atlassian’s deprecation window is roughly fifteen months from announcement to removal, which is at the long end of what’s reasonable.

The single most important operational practice during migration is logging the transport used on every request. You need to know when SSE traffic has actually stopped before you remove the endpoint. Servers that “turned off SSE on the announced date” and broke clients they didn’t know existed are a recurring pattern in 2026.

Failure Modes

Patterns that show up in production transport problems:

The silent stdio log corruption. A stdio server that writes any log output to stdout — even one line — corrupts the JSON-RPC stream and breaks the connection in ways that produce cryptic client errors. The fix is to disable Spring Boot’s startup banner and route all logging to stderr or a file. The first article in this publication shows the relevant application.yml snippet. This bites every team that builds a stdio server at least once.

The SSE idle timeout cascade. SSE connections held open through a load balancer with a thirty-second or sixty-second idle timeout drop silently and never reconnect cleanly. Symptoms: tool calls that succeed in development and fail in production, errors like “No connection established for request ID” or “SSE error: Non-200 status code.” The fix is to migrate to Streamable HTTP. There is no stable workaround within SSE itself.

The wrong starter dependency. Using spring-ai-starter-mcp-server (the stdio starter) when you mean to deploy a remote server, or spring-ai-starter-mcp-server-webmvc when you mean to use stdio. The starters are not interchangeable — each pulls in its own transport implementation. Symptom: the server starts but the endpoint isn't there, or the server tries to attach to stdin in a containerized deployment.

Forgetting to set protocol: STREAMABLE. The Spring AI webmvc starter defaults to the deprecated SSE transport for backward compatibility. A server deployed without the explicit protocol property is an SSE server even when the team thought they were building a Streamable HTTP server. Symptom: ChatGPT can't connect, modern clients complain about transport mismatch, the server doesn't appear in registries that filter by transport.

The “we’ll migrate later” deferral. Teams running SSE servers in production who keep pushing the Streamable HTTP migration. The deadline always feels distant until upstream services start removing SSE and the team realizes they’re now blocking, not just delayed. The migration is straightforward; defer it at your own risk.

Security Considerations

The transport choice has direct security implications.

stdio inherits the user’s trust boundary. A stdio server runs as a child of the host process, which runs as the user. Anything the user can do on the local machine, the server can do — read files, network access, environment variables. There is no authentication because there is no identity boundary to enforce. This is fine when the trust model matches (the user is running a tool against their own machine) and dangerous when it doesn’t (a stdio server bundled with a host application that doesn’t make clear what the server can access).

Streamable HTTP needs explicit authentication. A bare Streamable HTTP server exposed on the public internet with no auth is the MCP equivalent of an open database. The Spring AI community security module is the path of least resistance for Spring AI servers. The minimum bar is OAuth 2.0 bearer tokens with a real issuer; API keys are acceptable for internal-only deployments behind a corporate network boundary. We cover OAuth 2.1 specifically — including the resource indicator requirements that prevent token-confusion attacks — in a dedicated article later in the publication.

SSE has accumulated CVEs. Beyond the operational fragility, SSE’s split-endpoint design made it hard to apply consistent security policies. The two endpoints had to coordinate session state, which created room for session-fixation and request-smuggling vulnerabilities. Streamable HTTP’s single-endpoint design closed this surface. The security improvements were one of the explicit reasons for the deprecation, not just operational concerns.

Logging transport metadata is a security control, not just an ops one. Knowing which transport, which client, and which client version connected to your server matters for incident response. A request that arrived on the deprecated SSE endpoint from a client claiming a 2024–11–05 protocol version is a different risk profile than the same request on Streamable HTTP from a current client. Log it, store it, alert on it.

What Didn’t Work (And Why)

Three things I tried during migrations that didn’t pan out.

Trying to write a custom transport adapter that translated between SSE and Streamable HTTP at the application layer. The intuition was that we could keep one server and route both transports through a shared message handler. The result was a maintenance nightmare. The two transports differ in session semantics, error handling, and reconnection behavior in ways that a thin adapter cannot hide. The pattern that worked was running two server processes side by side, sharing tool implementations as a library, fronted by a proxy. Two processes are simpler than one process pretending to be two.

Underestimating how long external clients take to migrate. When the team announced a thirty-day deprecation window for an internal SSE endpoint, ninety percent of clients migrated in the first two weeks. The remaining ten percent took four months. Some of those clients were unaware they used MCP — their host applications had MCP servers bundled. The lesson: the long tail of MCP client migrations is much longer than the active development tail. Plan for the slowest client, not the median one.

Skipping the proxy and using application-level path routing. Tried to host SSE endpoints and the Streamable HTTP endpoint inside the same Spring Boot application via path matching in a custom configuration. It almost worked. The session management semantics between the two transports clashed in subtle ways — SSE wanted long-lived sessions tied to the GET stream, Streamable HTTP wanted sessions tied to a header. Bugs surfaced under load, not in testing. The proxy pattern with two processes is operationally heavier but much easier to reason about, and the per-process configuration matches what Spring AI was designed for.

Where This Goes Next

The next article in the Beginner track covers the operational reality of running a Streamable HTTP server in production — TLS certificates, the .well-known metadata file, registry submission, and the deployment checklist that takes a localhost server to a real domain. After that, the article on the Stripe MCP server as a code reference for production design choices.

Once you have the transport decision settled and a deployed Streamable HTTP server with authentication, the rest of the production engineering work — multi-tenancy, rate limiting, observability, cost attribution — has a stable foundation to build on. Get the transport wrong and every later decision is constrained by what your transport can support. Get it right and the protocol mostly fades into the background where it belongs.

References

Companion code: github.com/production-mcp-notebook/03-mcp-transports. The repository contains three runnable projects — a stdio server, an SSE server (kept for reference, marked deprecated), and a Streamable HTTP server with OAuth 2.0 — plus the proxy configuration for the dual-transport migration pattern.

If you found this useful, a clap helps other engineers find it. Follow The Production MCP Notebook for new articles every Tuesday and Friday — we’re building the canonical reference for production MCP servers, from your first Spring AI server through enterprise gateway architecture. Got a pattern you want to see covered? Leave a response — reader questions drive the roadmap.

[embed]The MCP Mental Model : Why It’s Not REST for LLMs Where Java Developers Go Wrong on Their First MCP Serverblog.stackademic.com

[embed]MCP Tools, Resources, and Prompts : The 3 Primitives Why Most MCP Servers Use Only One and Pay 7x in Tokensblog.stackademic.com

[embed]Deploy Your First Production MCP Server with Spring AI TLS, OAuth, .well-known, and the Registry Submission Checklistmedium.com


메타데이터
post_id
0eb08100aac7
slug
mcp-transports-streamable-http-sse-migration-spring-ai-0eb08100aac7
url
https://blog.stackademic.com/mcp-transports-streamable-http-sse-migration-spring-ai-0eb08100aac7
canonical_url
https://blog.stackademic.com/mcp-transports-streamable-http-sse-migration-spring-ai-0eb08100aac7
author_url
https://medium.com/@pat.vishad
status
ok
fetched_at
2026-07-09 05:26:43