Why MCP initialize Fails: A Diagnostic Taxonomy
The initialize handshake is the front door of a stateful MCP server: a JSON-RPC 2.0 request POSTed over HTTP, answered by a result carrying the negotiated protocol version, the server's identity, and its capabilities. When it fails, the error you see is usually several layers removed from the actual cause — a 405 can mean three different things, a hang can be a transport mismatch, and a "failure" can be a server that is working exactly as specified. This is a taxonomy of the failure modes that show up in practice, organized as symptom → cause → fix, roughly in the order you should rule them out.
For what a successful probe looks like end to end — including the exact curl commands — see how to health-check a remote MCP server.
1. Wrong endpoint path
Symptom: 404, or a 200 whose body is HTML (your app's index page, a marketing site, a framework error page). The JSON-RPC parse fails because there is no JSON-RPC there.
Cause: MCP endpoints have no single mandated path. /mcp is the common convention, but servers ship at /sse, /api/mcp, /v1/mcp, or wherever the framework mounted them. If you guessed the path, you probably guessed wrong.
Fix: Check the origin's /.well-known/mcp.json — a server card there typically declares the real endpoint URL. Failing that, check the server's docs or source. Treat "reachable but returns HTML" as not an MCP server at this path, never as "MCP server is down."
2. GET instead of POST
Symptom: 405 Method Not Allowed, or an unexpected response body.
Cause: initialize is a POST. A GET to a Streamable HTTP endpoint is reserved for opening an optional server-push stream — and the spec explicitly permits a request/response-only server to answer that GET with 405. Merlonix's own MCP server does exactly this: POST a JSON-RPC message to /mcp, get a single JSON response; GET gets a 405 by design.
Fix: Send POST. And do not let a monitoring tool interpret "GET returned 405" as downtime — for many healthy servers it is the specified behavior.
3. POST answered with 405
Symptom: The mirror image — your POST to the endpoint gets 405.
Cause: You are probably talking to a legacy HTTP+SSE transport server. In that older transport, the URL you have opens an event stream via GET, and JSON-RPC messages go to a different endpoint that the server announces on the stream. POSTing the stream URL is a method the server does not accept.
Fix: Either open the SSE stream properly (GET with Accept: text/event-stream, read the endpoint event, POST messages there) or, better, confirm which transport the server actually speaks — Streamable HTTP vs SSE covers how to tell from the outside.
4. Missing or wrong Accept header
Symptom: 406 Not Acceptable, or a 400 with a complaint about acceptable content types — from a server that works fine for real MCP clients.
Cause: The Streamable HTTP transport lets a server answer a POST with either application/json or a text/event-stream frame, and the spec expects the client to advertise that it can handle both. Strict servers (several SDK implementations included) validate this and reject a POST whose Accept header does not list both types.
Fix: Send Accept: application/json, text/event-stream on every request — including initialize.
5. Protocol-version mismatch
Symptom: A JSON-RPC error mentioning the protocol version; or a handshake that succeeds but returns a protocolVersion different from the one you sent; or a result with no protocolVersion at all.
Cause: Client and server each support a set of dated spec revisions (2024-11-05, 2025-03-26, 2025-06-18, …). Well-behaved servers echo your requested version when they support it and otherwise answer with their preferred one — that is negotiation, not failure. Some stricter servers reject outright. A result missing protocolVersion entirely is a spec-compliance bug on the server: some clients refuse a handshake without it.
Fix: As a client, treat a different echoed version as a signal to adapt or bail cleanly, not as an error. As a server author, always echo a version, and support more than one dated revision if you can.
6. 401/403 — gated, not broken
Symptom: HTTP 401 or 403 before any JSON-RPC gets processed.
Cause: The server requires a credential. This is the failure mode most often misdiagnosed as an outage: the server is up and healthy; it just will not talk to an anonymous caller.
Fix: Attach Authorization: Bearer <token> and retry. In monitoring terms, classify this as reachable-but-auth-gated (degraded visibility), and alert on the transition — a server that was open yesterday and gated today, or the reverse, is the actual signal.
7. Hitting an SSE endpoint and hanging
Symptom: The request neither completes nor errors — it hangs until your timeout, possibly with text/event-stream in the response headers and no JSON-RPC body.
Cause: You opened a legacy SSE stream (or a server-push stream) and are waiting for a request/response that will never arrive on that channel. Long-lived streams also interact badly with proxies and short client timeouts.
Fix: Same as case 3 — establish which transport this is first. A health probe should use a bounded timeout precisely so a stuck stream degrades to a clean failure instead of hanging the check.
8. CORS — browser clients only
Symptom: fetch from a web app fails with a CORS error, but the identical request from curl or a server-side client works.
Cause: Browsers enforce cross-origin policy; curl does not. The server is fine on the wire — it just does not send the CORS response headers (or handle the OPTIONS preflight) that a browser-based MCP client needs. Mcp-Session-Id adds a subtlety: it is a custom header, so the server must also expose it via Access-Control-Expose-Headers or browser clients cannot read it even when the request succeeds.
Fix: If your clients are browser-based, add CORS handling server-side. If they are not, this failure mode does not apply to you — and it should never be diagnosed from a browser alone.
9. Body is not JSON-RPC 2.0
Symptom: JSON-RPC error -32700 (parse error) or -32600 (invalid request), or a 400.
Cause: The envelope is wrong: missing "jsonrpc": "2.0", missing id, params malformed, or a Content-Type other than application/json. Hand-built requests and shell-quoting accidents are the usual culprits.
Fix: Send exactly the shape in the spec — jsonrpc, id, method: "initialize", and params with protocolVersion, capabilities, and clientInfo.
10. Session-id requirements on stateful servers
Symptom: initialize succeeds, but every subsequent call (tools/list, tools/call) gets a 400/404 or a "server not initialized" JSON-RPC error.
Cause: Stateful Streamable HTTP servers may issue an Mcp-Session-Id response header on initialize and require it on every later request. Separately, strict servers expect the notifications/initialized notification after the handshake before they will accept normal traffic. Dropping either breaks everything after the handshake while the handshake itself looks fine.
Fix: Capture Mcp-Session-Id from the initialize response and replay it as a request header on all follow-ups; fire notifications/initialized (no id — it is a notification) before your first real call.
11. The server has no initialize at all (2026-07-28 stateless spec)
Symptom: initialize is rejected or unrecognized, yet the server is demonstrably serving agents.
Cause: The 2026-07-28 spec revision made the protocol stateless: it removed the initialize/initialized handshake and the Mcp-Session-Id header. Client info and protocol version travel per-request in _meta, and requests carry Mcp-Method/Mcp-Name routing headers. A migrated server failing your handshake is behaving correctly.
Fix: On a handshake failure against a reachable, non-auth-gated endpoint, probe tools/list directly before concluding anything. If it answers with a usable tool list, the server is healthy on the stateless core — a diagnosis path any post-2026 client or monitor needs built in.
Debugging Order
Rule things out cheapest-first: path (1), method (2–3), headers (4), then the response itself — HTTP status (6), JSON-RPC envelope (9, 5), and finally state (10) and spec generation (11). CORS (8) only ever applies to browser clients, so confirm from curl before touching server config.
Or skip the manual bisection: the free MCP health checker runs this whole decision tree against any endpoint URL — handshake, version echo, transport read, tools/list, the stateless fallback, and auth classification — and tells you which of these cases you are in.