# Remote MCP server not connecting? Test it by hand

> How to test a remote MCP server with curl under the 2026-07-28 spec and the older session-based one, and what each HTTP status code tells you.

Published 2026-10-16. Source: https://toolvet.app/blog/remote-mcp-server-not-connecting

**Short answer:** send one POST to the server URL with the headers the current MCP spec requires and a `tools/list` body. A JSON or SSE response with a tool list means the server is up and speaks the 2026-07-28 protocol. A `400` whose body is one of the spec's modern JSON-RPC errors means it is up and your request needs fixing; a `401` means it is up and wants a login; a `400` without such an error, a `404` or a `405` points to an older server that still expects the `initialize` handshake, so fall back to that. As of 16 October 2026, ToolVet runs these handshakes on a schedule against 28,233 listed remote MCP servers with uptime data; 23,462 of them (83.1%) have a 30-day uptime of 99.9% or more, and 4,771 (16.9%) are below 95%.

Some background: a remote MCP server is an HTTP endpoint, typically `https://example.com/mcp`, that an agent talks to with JSON-RPC over the Streamable HTTP transport. When a client refuses to connect, the error it shows is often not the real one: a client that follows the spec's fallback tries the 2026-07-28 request, then `initialize`, then the old GET, and the error it shows may belong to the last attempt. The fix is to make the requests yourself, one generation at a time, and read the raw status codes. ToolVet does this on a schedule for every [listed remote MCP server](https://toolvet.app/browse?kind=mcp), and the free [MCP scanner](https://toolvet.app/mcp-scanner) does it once for any URL you paste, then checks the tool definitions for prompt injection.

## Two generations of the same transport

The [2026-07-28 revision](https://modelcontextprotocol.io/specification/2026-07-28/changelog) changed Streamable HTTP in ways that matter for a connection test:

- The `initialize` / `notifications/initialized` handshake is gone. Every request carries its protocol version and client capabilities in `params._meta`.
- Protocol-level sessions and the `Mcp-Session-Id` header are gone.
- The HTTP GET stream is gone; a server may answer GET with `405`.
- Every POST must carry `MCP-Protocol-Version` and `Mcp-Method` headers, plus `Mcp-Name` on `tools/call`, `resources/read` and `prompts/get`.
- A new `server/discover` request lets a client ask for supported versions and capabilities up front.

Protocol versions [2025-03-26 through 2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports) used the same endpoint but with the handshake, optional sessions and the GET stream. The older HTTP+SSE transport from 2024-11-05, with its separate SSE and POST endpoints, is deprecated and still deployed. A test has to cover all three.

## Step 1: the modern request

```bash
curl -s -D - https://example.com/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'mcp-protocol-version: 2026-07-28' \
  -H 'mcp-method: tools/list' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{
        "io.modelcontextprotocol/protocolVersion":"2026-07-28",
        "io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0"},
        "io.modelcontextprotocol/clientCapabilities":{}}}}'
```

The `Accept` header must list both content types, because the [transport spec](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) lets the server answer a request either with one JSON object (`Content-Type: application/json`) or with an SSE stream (`text/event-stream`) that ends in the JSON-RPC response. Both are a pass. The `-D -` prints the response headers, which you need for the next step.

## Step 2: read the status code

| Response | What it means | Server up? |
|---|---|---|
| `200`, JSON or SSE with `result.tools` | Current protocol, working | Yes |
| `400`, body is a JSON-RPC error with code `-32022` | Up, but it doesn't support 2026-07-28; the error lists the versions it does | Yes |
| `400`, body is a JSON-RPC error with code `-32020` | Up; your headers don't match the body (missing `Mcp-Method`, wrong version header) | Yes |
| `400`, body empty or not one of the modern error codes (`-32020`, `-32021`, `-32022`) | Probably an older server; go to step 3 | Unknown yet |
| `401` | Up; it wants OAuth or an API key | Yes |
| `402` | Up; it wants payment, for example over x402 | Yes |
| `403` | Up; a firewall, proxy or auth gate refused you, or the server rejected an `Origin` header you sent | Yes |
| `404`, body is a JSON-RPC error with code `-32601` | Up; a current server that doesn't implement the method you asked for | Yes |
| `404` or `405`, no JSON-RPC error | Wrong path, or an HTTP+SSE server; go to step 4 | Unknown yet |
| Connection refused, timeout, TLS error, `5xx` | Down, or unreachable from where you are | No |

The spec is explicit about the `400` case: a client "SHOULD inspect the response body before falling back", because modern servers also use `400` for an unsupported version (`-32022`), a missing client capability (`-32021`) and header validation (`-32020`). A `-32022` body is the server telling you which versions to retry with. Only a body that is empty or carries none of these errors justifies the fallback.

One code trips people up. A `401` is not a failure: the server is up and wants credentials, which a test from a terminal doesn't have; ToolVet counts a login or payment request as answering for the same reason. (`403` is similar when it comes from the server itself: the spec requires a `403` for an invalid `Origin` header, to block DNS rebinding.)

## Step 3: the handshake fallback

Older servers need `initialize` first. Send it with the latest version of that generation, and watch the response headers for `Mcp-Session-Id`:

```bash
curl -s -D - https://example.com/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
        "protocolVersion":"2025-11-25","capabilities":{},
        "clientInfo":{"name":"curl","version":"1.0"}}}'
```

A `200` with `result.protocolVersion` and `result.serverInfo` means the server is up and speaks this generation. If it also sent an `Mcp-Session-Id` header, every later request must repeat that header, and the [2025-11-25 transport rules](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports) say a server that requires sessions should answer `400` when it is missing and `404` once the server has ended the session; on a `404` the client must start over with a new `initialize`. So a `404` that appears after a working `initialize` is a session problem, not a dead server.

The client then confirms with a `notifications/initialized` POST, which the server answers with `202 Accepted` and no body, and lists the tools, with the negotiated version in the `MCP-Protocol-Version` header and the session id if it got one (drop that line if `initialize` returned no `Mcp-Session-Id`):

```bash
curl -s https://example.com/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'mcp-protocol-version: 2025-11-25' \
  -H 'mcp-session-id: <id from the initialize response>' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
```

## Step 4: the HTTP+SSE fallback

If POST gave you a `400`, `404` or `405` without one of the modern JSON-RPC errors, the spec's fallback is a GET: `curl -N -H 'accept: text/event-stream' https://example.com/mcp`. A server on the 2024-11-05 transport opens a stream and sends an `endpoint` event with the URL to POST to. A stream with no `endpoint` event is a 2025-era standalone stream: stop it and go back to step 3. A `405` means a Streamable HTTP server of any revision that offers no GET stream, so your earlier POST failed for another reason: recheck the path and the body. HTML means the URL is a web page, not an MCP endpoint.

## Things that break a working server

- **Redirects.** A server that redirects `/mcp` to `/mcp/`, or `http` to `https`, works in a browser and can fail in a client whose HTTP library turns the redirected POST into a GET, which [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4) allows for `301`, `302` and `303`. ToolVet follows up to two redirects, only on the same host, and never from HTTPS to HTTP; a server that needs more than that counts as failed.
- **Proxies buffering SSE.** The spec tells servers to send `X-Accel-Buffering: no` on SSE responses so reverse proxies such as nginx pass events through immediately. Without it, a streamed `tools/list` can sit in a buffer until the client gives up.

## How ToolVet decides a server is up

Each listed remote MCP server gets a scheduled check that runs the handshake (`initialize`, then `tools/list`), falling back to the 2026-07-28 request form when a server rejects the handshake as legacy, and records the exposed tools, with the user agent `ToolVetProbe/1.0 (+https://toolvet.app/methodology)`. A server that asks for a login or a payment counts as reachable. Two failed checks in a row open an outage; the next passing check closes it. Verified, featured and actively used listings are checked every 30 minutes, the rest every few hours, so outage timing is only as precise as the check interval. The [methodology page](https://toolvet.app/methodology) has the details, including what happens when many listings fail in the same round.

What that produces, as of 16 October 2026, the first day of ToolVet's published [daily history](https://toolvet.app/data):

| 30-day uptime | Remote MCP servers | Share |
|---|---|---|
| 99.9% or more | 23,462 | 83.1% |
| 95% to under 99.9% | 0 | 0% |
| Under 95% | 4,771 | 16.9% |

Uptime is the share of passed checks over 30 days, so a listing with few checks moves in large steps, and the empty middle band probably reflects how young many listings' check histories are. Every listing has a status page at `/status/<listing>` with an Atom feed, and `GET /api/v1/watch?ids=` returns outages for up to 25 listings in one free call, so an agent can skip a server that is down before it tries to connect.

## Frequently asked questions

### Is a 401 from a remote MCP server an error?

No. It means the server answered and wants credentials, for example OAuth under the MCP authorization spec. The server is up; your client needs to complete the login flow. ToolVet counts a login or payment request as a passing check for the same reason.

### How do I find out which MCP protocol version a remote server supports?

Send a 2026-07-28 request. A current server answers it, or, if it doesn't support that version, answers `400` with a JSON-RPC error of code `-32022` that lists the versions it supports; the `server/discover` method, required of 2026-07-28 servers, returns the same list on request. An older server answers `400`, `404` or `405` without a recognised modern error body: send `initialize` and read `protocolVersion` from its result.

### Why does my MCP client get a 404 after connecting?

Under protocol versions 2025-03-26 to 2025-11-25 a server could assign a session in the `Mcp-Session-Id` header and answer `404` once that session ended; the client must then start over with a new `initialize`. The 2026-07-28 revision removed sessions, so if you see this you are talking to an older server, and reconnecting is the correct fix.

### Does an HTTP 200 on GET mean a remote MCP server is up?

No. The current spec removed the GET stream, so a healthy 2026-07-28 server may answer GET with `405`, and a `200` with HTML means you hit a web page. What proves a server is up is a POST that returns a JSON-RPC result, or a `401` or `402` asking for credentials or payment.

### Can I monitor a remote MCP server without running checks myself?

If it is listed on ToolVet, its status page at `/status/<listing>` shows every check and has an Atom feed for outages and recoveries; if it isn't, [submit it](https://toolvet.app/submit) for a free check.

[How we check and score](https://toolvet.app/methodology) explains the rest.
