Guides · Published November 9, 2026

Remote MCP setup: Streamable HTTP vs SSE, plus OAuth

Most remote MCP trouble is one of three things: the wrong transport, a missing OAuth discovery document, or a client that connects from somewhere your server cannot be reached. Here is how the pieces fit, straight from the MCP spec (read November 9, 2026).

Pick the transport first

Short answer: build for Streamable HTTP. It is the one the spec defines for remote servers. The older HTTP+SSE transport (from the 2024-11-05 revision) has been deprecated since 2025-03-26, and the spec says new implementations should not adopt it and existing ones should migrate. Only keep an SSE endpoint around if you know you have old clients.

What Streamable HTTP looks like in the latest revision (2026-07-28):

  • One MCP endpoint, like https://example.com/mcp, that accepts POST.
  • Every JSON-RPC request or notification is its own POST. The client sends an Accept header listing both application/json and text/event-stream.
  • For a request, the server answers with either one JSON object or an SSE stream scoped to that request. Notifications get a 202 Accepted with no body.
  • Each POST carries an MCP-Protocol-Version header, plus Mcp-Method and (for tool calls, resource reads and prompt gets) Mcp-Name. The server has to reject a request whose headers disagree with the body, with a 400 and error code -32020.
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather

Heads up if you learned this a while ago: the spec’s own notes say revision 2026-07-28 removed the GET stream endpoint and protocol-level sessions. Revisions 2025-03-26 through 2025-11-25 used the Mcp-Session-Id header, an optional GET stream, and resumable streams with Last-Event-ID. A server that only speaks the new revision should answer GET or DELETE on the endpoint with 405 and ignore session and event-ID headers. If you need to serve older clients too, follow the spec text for the revision they speak.

Old clients and old servers

The spec’s fallback recipe for a client that does not know which transport a URL uses: POST first. If that works, it is Streamable HTTP. If it fails with 400, 404 or 405 and the body is not a recognized modern JSON-RPC error, do a GET on the same URL and look for an endpoint event as the first event. If you get one, it is the old HTTP+SSE transport.

Things the spec asks of your server

  • Check the Origin header. If it is present and not valid, respond 403. This is the DNS rebinding protection.
  • Bind to localhost (127.0.0.1) when running locally, not 0.0.0.0.
  • Add X-Accel-Buffering: no on SSE responses so reverse proxies like nginx do not buffer events.
  • For long-lived streams, the spec suggests an occasional SSE comment line as a keep-alive.

Which clients speak what

Remote transport support by client
ClientStreamable HTTPLegacy SSESource
Claude (custom connectors)Yes, recommendedSupported, being deprecatedClaude docs
CursorYesYesCursor docs
VS CodeTries it firstFallbackVS Code docs
CodexYesNot statedOpenAI docs

Streamable HTTP works everywhere on that list. That is why it is the safe default.

OAuth for remote servers, in the order it happens

Authorization is optional in MCP. If your server needs sign-in over HTTP, the spec says to follow its OAuth-based flow. (For stdio servers it says to pull credentials from the environment instead.) Your MCP server acts as an OAuth resource server, and the thing that issues tokens is a separate role, the authorization server. They can be the same service or not.

  1. Client calls your endpoint with no token. You answer 401 with a WWW-Authenticate header that points at your protected resource metadata, and ideally the scope needed.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         scope="files:read"
  1. Serve protected resource metadata (RFC 9728). The spec says MCP servers must implement it and clients must use it to find your authorization server.
  2. The authorization server must publish metadata, either OAuth 2.0 Authorization Server Metadata (RFC 8414) or OpenID Connect Discovery. Clients must support both.
  3. The client gets a client ID. Three ways, in the spec: a Client ID Metadata Document (the spec says clients and servers should support it), a pre-registered client, or Dynamic Client Registration. The spec marks DCR as deprecated and kept for servers that do not support metadata documents. Before you pick one, check what your target clients do: Claude can use a metadata document Anthropic hosts, DCR, or a client ID you give it; Codex picks metadata documents when the server advertises them and falls back to DCR.
  4. Authorization code flow with PKCE, and the client must send the resource parameter (RFC 8707) in both the authorization request and the token request, set to your server’s canonical URI, like https://mcp.example.com/mcp.
  5. The client records the issuer and checks the iss value on the redirect (RFC 9207) before it spends the authorization code.
  6. Every request after that carries Authorization: Bearer <token>. The spec says it goes in the header on every HTTP request and never in the URL query string.

Server rules that cause the most bugs

  • Validate the audience. Accept only tokens issued for your server, and do not accept or pass through tokens meant for something else.
  • 401 for missing or invalid tokens, 403 for not enough scope. For the 403 case, the spec says to send error="insufficient_scope" and the scopes needed for the current operation, all in one challenge instead of one at a time.
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
                         scope="files:write",
                         resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"
  • Scopes: put the scope the request needs in the 401 challenge. Clients are told to prefer that over guessing from scopes_supported.
  • Do not put offline_access in your challenge or scopes_supported. The spec says refresh tokens are not a resource requirement, and clients that want them may ask for it themselves.

Per-client checklist

Redirect and limits to plan for
ClientRedirect / callback to allowAlso know
Claude (hosted apps)https://claude.ai/api/mcp/auth_callbackConnects from Anthropic’s cloud, so the server must be reachable from the public internet. Docs: about 150,000 characters max per tool result and 240 seconds per tool call on claude.ai and Desktop. Resource subscriptions and sampling are not supported.
Claude CodeLoopback redirect on any port (the docs show http://localhost:3118/callback as an example)Match loopback without caring about the port.
Cursorhttps://www.cursor.com/agents/mcp/oauth/callback (web, Cursor Agents) and http://localhost:8787/callback (desktop)Static client ID goes in the auth block of mcp.json if you do not support DCR.
CodexLoopback, http://127.0.0.1/callback by default (it prints the exact URL when you add a pre-registered client)Tokens by env var also work: bearer_token_env_var in config.toml.
VS CodeNot stated in the docs we readOptional oauth.clientId in the VS Code format. Browser opens on first connect.

Test it before you blame the client

  1. Call the endpoint with no token and check you get a 401 with a WWW-Authenticate header that has resource_metadata. Then fetch that URL.
  2. Open the authorization server metadata and confirm the endpoints (and, if you rely on DCR, a registration endpoint) are listed.
  3. Run a real tool call through a client that is not the one you are debugging. The MCP Inspector on this site connects straight from your browser over Streamable HTTP or SSE. Because the request goes from your browser, the server has to allow cross-origin requests from the page; if it does not, the inspector says the browser blocked it and lets you paste a tools/list response instead.
  4. Run your tool definitions through the TDQS lint so the names and descriptions clients show are not an afterthought.

None of this replaces reading the spec revision your clients target. But if you get the transport, the 401 challenge and the metadata documents right, most clients will connect on the first try.

Sources

“Latest” in the spec URLs means revision 2026-07-28 as of the day we read it. The spec moves, so check the revision number when you copy a header or field name.