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
Acceptheader listing bothapplication/jsonandtext/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 Acceptedwith no body. - Each POST carries an
MCP-Protocol-Versionheader, plusMcp-Methodand (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_weatherHeads 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: noon 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
| Client | Streamable HTTP | Legacy SSE | Source |
|---|---|---|---|
| Claude (custom connectors) | Yes, recommended | Supported, being deprecated | Claude docs |
| Cursor | Yes | Yes | Cursor docs |
| VS Code | Tries it first | Fallback | VS Code docs |
| Codex | Yes | Not stated | OpenAI 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.
- Client calls your endpoint with no token. You answer 401 with a
WWW-Authenticateheader 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"- Serve protected resource metadata (RFC 9728). The spec says MCP servers must implement it and clients must use it to find your authorization server.
- The authorization server must publish metadata, either OAuth 2.0 Authorization Server Metadata (RFC 8414) or OpenID Connect Discovery. Clients must support both.
- 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.
- Authorization code flow with PKCE, and the client must send the
resourceparameter (RFC 8707) in both the authorization request and the token request, set to your server’s canonical URI, likehttps://mcp.example.com/mcp. - The client records the issuer and checks the
issvalue on the redirect (RFC 9207) before it spends the authorization code. - 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_accessin your challenge orscopes_supported. The spec says refresh tokens are not a resource requirement, and clients that want them may ask for it themselves.
Per-client checklist
| Client | Redirect / callback to allow | Also know |
|---|---|---|
| Claude (hosted apps) | https://claude.ai/api/mcp/auth_callback | Connects 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 Code | Loopback redirect on any port (the docs show http://localhost:3118/callback as an example) | Match loopback without caring about the port. |
| Cursor | https://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. |
| Codex | Loopback, 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 Code | Not stated in the docs we read | Optional oauth.clientId in the VS Code format. Browser opens on first connect. |
Test it before you blame the client
- Call the endpoint with no token and check you get a 401 with a
WWW-Authenticateheader that hasresource_metadata. Then fetch that URL. - Open the authorization server metadata and confirm the endpoints (and, if you rely on DCR, a registration endpoint) are listed.
- 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/listresponse instead. - 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.
- MCP spec: Streamable HTTP transport (latest) · read Nov 9, 2026
- MCP spec: transports overview (latest) · read Nov 9, 2026
- MCP spec: authorization (latest) · read Nov 9, 2026
- Claude docs: build an MCP server for Claude · read Nov 9, 2026
- Claude docs: authentication for connectors (callback URLs) · search excerpt read Nov 9, 2026
- Claude Help Center: custom connectors using remote MCP · search excerpt read Nov 9, 2026
- Cursor docs: MCP · read Nov 9, 2026
- OpenAI docs: Model Context Protocol (Codex) · read Nov 9, 2026