Introduction

Stdio transport works well for local servers, but what about servers running on a remote machine or in the cloud? MCP provides two HTTP-based transports for remote connections: the legacy SSE (Server-Sent Events) transport and the newer Streamable HTTP transport. Each serves different deployment scenarios, and knowing when to use which is essential for building production MCP clients.

Key Concepts

  • SSEClientTransport: A legacy transport from @modelcontextprotocol/sdk/client/sse.js that uses Server-Sent Events for server-to-client streaming and HTTP POST for client-to-server messages. Supported by older MCP servers.
  • StreamableHTTPClientTransport: The recommended transport from @modelcontextprotocol/sdk/client/streamableHttp.js for remote servers. Uses HTTP POST for all messages with optional streaming responses. Supports session management via the MCP-Session-Id header and protocol versioning via the MCP-Protocol-Version header.
  • Session Management: Remote transports track sessions using the MCP-Session-Id header. The server assigns a session ID during initialization, and the client includes it in subsequent requests.
  • Authentication: HTTP transports can include authentication headers (Bearer tokens, API keys) with every request, enabling secure connections to protected servers.

Real World Context

Your team has deployed an MCP server as a shared service behind an API gateway. Multiple developers' AI tools need to connect to it over the network. You cannot use stdio because the server is not a local process. Instead, you use StreamableHTTPClientTransport to connect over HTTPS, passing an API key in the Authorization header. The transport handles session management automatically, and the server can serve multiple concurrent clients.

Deep Dive

SSE Transport (Legacy)

The SSE transport was the original HTTP transport for MCP. It uses Server-Sent Events for server-to-client communication and HTTP POST for client-to-server messages.

typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";

const client = new Client({
  name: "my-remote-client",
  version: "1.0.0"
});

const transport = new SSEClientTransport(
  new URL("http://localhost:3001/sse")
);

await client.connect(transport);

The SSE transport connects to the server's SSE endpoint, which streams events from the server to the client. Client-to-server messages are sent as POST requests to a separate endpoint that the server communicates during the SSE handshake. While functional, this two-endpoint approach is being replaced by the simpler Streamable HTTP transport.

The Streamable HTTP transport is the modern approach for remote MCP connections. It uses a single endpoint for all communication.

typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const client = new Client({
  name: "my-remote-client",
  version: "1.0.0"
});

const transport = new StreamableHTTPClientTransport(
  new URL("http://localhost:3001/mcp")
);

await client.connect(transport);

The key difference is that Streamable HTTP uses a single /mcp endpoint. The client sends JSON-RPC messages as HTTP POST requests, and the server can respond with either a regular JSON response or a streaming response for long-running operations.

Adding Authentication

Remote servers typically require authentication. Both HTTP transports accept custom headers through a request init object or a custom fetch function.

With StreamableHTTPClientTransport, you can pass headers via the constructor options:

typescript
const transport = new StreamableHTTPClientTransport(
  new URL("https://api.example.com/mcp"),
  {
    requestInit: {
      headers: {
        Authorization: "Bearer sk-your-api-key"
      }
    }
  }
);

await client.connect(transport);

The requestInit object is passed to every fetch call the transport makes, so your authentication headers are included with every request.

Session Management with MCP-Session-Id

Streamable HTTP transport supports session management through the MCP-Session-Id header. Here is how it works:

text
1. Client sends initialize request (no session ID)
2. Server responds with MCP-Session-Id header
3. Client includes MCP-Session-Id in all subsequent requests
4. Server uses session ID to maintain state per client

The transport handles this automatically. You do not need to manage session IDs manually. The server assigns the ID during initialization, and the transport stores it and sends it with every request.

Session IDs enable the server to maintain per-client state, such as cached data, transaction contexts, or user preferences.

Protocol Version Header

In addition to the session ID, clients MUST include the MCP-Protocol-Version header on all subsequent HTTP requests after initialization. This header tells the server which protocol version to use for the response. The value should be the version negotiated during the initialization handshake.

text
POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Session-Id: abc123
MCP-Protocol-Version: 2025-11-25

If the server receives an invalid or unsupported version, it responds with 400 Bad Request. The transport handles this automatically in the SDK, but custom HTTP implementations must include it manually.

Session Termination

To terminate a session, the client sends a DELETE request to the /mcp endpoint with the MCP-Session-Id header:

text
DELETE /mcp HTTP/1.1
MCP-Session-Id: abc123

This explicitly ends the session on the server side. The client.close() method in the SDK handles this automatically for Streamable HTTP connections.

Choosing Between Transports

Here is a decision guide for which transport to use:

text
Scenario                          Transport
────────────────────────────────────────────────────
Local server, client manages      StdioClientTransport
  process lifecycle

Remote server, modern MCP         StreamableHTTPClientTransport
  (recommended)

Remote server, legacy MCP         SSEClientTransport
  (older servers only)

The Streamable HTTP transport is the recommended choice for any remote server. Only use the SSE transport if you are connecting to an older server that does not support the Streamable HTTP protocol.

Fallback Pattern: Streamable HTTP to SSE

Some clients implement a fallback strategy when connecting to servers of unknown version:

typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";

const client = new Client({ name: "my-app", version: "1.0.0" });
const url = new URL("http://localhost:3001/mcp");

try {
  const transport = new StreamableHTTPClientTransport(url);
  await client.connect(transport);
} catch (error) {
  // Fallback to SSE for older servers
  const sseUrl = new URL("http://localhost:3001/sse");
  const sseTransport = new SSEClientTransport(sseUrl);
  await client.connect(sseTransport);
}

This pattern tries the modern Streamable HTTP transport first and falls back to SSE if the server does not support it. This is useful when your client needs to work with servers of varying ages.

Common Pitfalls

  1. Using SSE for new deployments — The SSE transport is legacy. New servers should use Streamable HTTP, and new clients should prefer it. Only fall back to SSE for backward compatibility.
  2. Forgetting authentication on remote servers — Unlike stdio (which inherits process-level security), HTTP transports send messages over the network. Always use HTTPS and include authentication headers for production deployments.
  3. Ignoring session management — If a server assigns a session ID and the client does not send it back, the server may reject requests or lose state. The transport handles this automatically, but custom HTTP implementations must track the MCP-Session-Id header.

Best Practices

  1. Default to Streamable HTTP for remote servers — It is simpler (single endpoint), supports streaming, and has built-in session management.
  2. Always use HTTPS in production — MCP messages may contain sensitive data (API keys, database queries, file contents). Never use plain HTTP for production deployments.
  3. Implement the fallback pattern — If your client needs to connect to servers you do not control, try Streamable HTTP first and fall back to SSE. This maximizes compatibility.

Summary

  • SSEClientTransport is the legacy HTTP transport using Server-Sent Events for streaming. Use it only for older servers.
  • StreamableHTTPClientTransport is the recommended transport for remote connections, using a single endpoint with optional streaming.
  • Authentication is added via requestInit.headers in the transport constructor options.
  • Session management via MCP-Session-Id is handled automatically by the Streamable HTTP transport.
  • Use stdio for local servers, Streamable HTTP for remote servers, and SSE only as a fallback.

Code Examples

typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const client = new Client({ name: "my-app", version: "1.0.0" });

const transport = new StreamableHTTPClientTransport(
  new URL("https://api.example.com/mcp"),
  {
    requestInit: {
      headers: {
        Authorization: "Bearer sk-your-api-key"
      }
    }
  }
);

await client.connect(transport);
✓ Completed