Test an MCP Server with curl: initialize, tools/list, Errors | MCP Hunter

Test an MCP Server with curl: initialize, tools/list, Errors

The exact curl requests to test an MCP server over Streamable HTTP, for the 2025-11-25 handshake and the 2026-07-28 revision, and how to read every response.

MCP Hunter team 7 min read

An MCP server over HTTP is one URL that takes JSON-RPC in a POST, so curl can test it with no SDK and no Inspector. Which requests you send depends on the protocol revision the server speaks, and almost every server today speaks the older one: of 694 registry servers that completed a handshake on 20 August 2026, 382 answered on 2025-11-25 and 3 on the current 2026-07-28 (protocol revision).

Every request below was run against our own MCP server at https://mcp-hunter.com/mcp on 3 October 2026, and the responses quoted are what it returned. They follow the spec text for both revisions [1][2][3], and they are the same requests our connection tester sends.

TL;DR:

  • Every POST needs Accept: application/json, text/event-stream. Both types, always. The server picks which one to answer with [1].
  • On 2025-11-25 it is three calls: initialize, then notifications/initialized, then tools/list, carrying the Mcp-Session-Id the first response handed you [1].
  • On 2026-07-28 there is no handshake. Each request carries its version in _meta and in the MCP-Protocol-Version and Mcp-Method headers [2].
  • Try the modern request first, and fall back when the answer is not one of the three modern error codes. That is how a client tells the two apart [2][3].

Set the URL once

URL=https://your-server.example.com/mcp

Use the endpoint itself, the URL a client POSTs to, not your docs page. If your server needs a token, add -H "Authorization: Bearer $TOKEN" to every request below.

The 2025-11-25 handshake

1. initialize

curl -sS -D headers.txt "$URL" \
  -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.0"}}}'

A healthy server answers with the revision it chose, what it offers and who it is. Ours, shortened:

{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{"listChanged":false}},"serverInfo":{"name":"MCP Hunter","version":"1.0.0"}}}

The server picks the version. If it answers with an older one than you asked for, use that one from here on.

-D headers.txt saved the response headers, because the session id is in a header, not in the body. A server may assign one [1]:

SESSION=$(grep -i '^mcp-session-id:' headers.txt | cut -d' ' -f2 | tr -d '\r')
echo "$SESSION"

Ours printed 65cdeffe-c687-4084-827c-40fcbf40c0fe. If yours prints nothing, the server does not use sessions: drop the Mcp-Session-Id line from the next two requests.

2. notifications/initialized

curl -sS -o /dev/null -w '%{http_code}\n' "$URL" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -H "Mcp-Session-Id: $SESSION" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

This is a notification, so it has no id and gets no JSON-RPC reply. The right answer is 202 with an empty body [1]. Ours printed 202.

3. tools/list

curl -sS "$URL" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -H "Mcp-Session-Id: $SESSION" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

The answer is result.tools, one object per tool. To print only the names, pipe it through jq -r '.result.tools[].name'. If the result also carries a nextCursor, there is another page: send the same request again with "params":{"cursor":"<that value>"} until it stops coming back. Every field a tool can carry is in the tools/list reference.

The 2026-07-28 requests

This revision drops initialize and sessions. Each request carries its own version and client details in params._meta, and mirrors the version and method name into headers [2]. Start with server/discover, which every 2026-07-28 server must answer [3]:

curl -sS "$URL" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: server/discover' \
  -d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{},"io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0.0"}}}}'

A modern server answers with result.supportedVersions. Then list the tools the same way, changing only the method in two places:

curl -sS "$URL" \
  -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":2,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{},"io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0.0"}}}}'

The MCP-Protocol-Version header has to match the _meta version, and Mcp-Method has to match method. A mismatch is rejected with 400 and error -32020 (HeaderMismatch) [2].

Which one is my server on?

Send the server/discover request first. Only two kinds of answer prove the server is modern [2][3]:

  • a result with supportedVersions
  • an error with code -32020, -32021 or -32022

Anything else means the server is on the handshake revisions, and you use the three calls above. Our own server is one of them. It answered:

{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"The method [server\/discover] was not found."}}

-32601 is plain "method not found", not a modern error, so the right move is to fall back to initialize. That is the same decision our tester makes on every check.

How to read what comes back

You got It means What to do
200, application/json A normal answer Read result or error
200, text/event-stream The server answered as a stream, which it may always do [1] The JSON is on the data: lines: add | sed -n 's/^data: //p'
202, empty body A notification was accepted Nothing. This is correct
401 or 403 The server is running and guards itself See the next section
400 on a request after initialize Often a missing Mcp-Session-Id: servers that use sessions should refuse requests without one [1] Copy the header from the initialize response
404 with a session id The session ended Send initialize again, without the old id [1]
400 with -32020 HeaderMismatch on 2026-07-28 Make the headers match the body [2]
405 on GET Normal. 2025-11-25 servers may refuse GET [1], and 2026-07-28 removed it [2] Nothing. Test with POST
405 or 404 on POST Not an MCP endpoint, or a server on the old HTTP+SSE transport Run the GET check below
An HTML page You called the docs or landing page Find the endpoint URL

The session error is the one people search for word for word: "Bad Request: Mcp-Session-Id header is required". It means the server handed out a session in step 1 and the request in front of it did not send it back. More on that header in Mcp-Session-Id.

Checking for the old HTTP+SSE transport. A server from the 2024-11-05 era refuses the POST and opens a stream on GET instead [1]:

curl -sS -N --max-time 5 "$URL" -H 'Accept: text/event-stream'

If the first lines say event: endpoint, the server works and is one transport behind. If you get a 405, an HTML page or nothing, the URL is not an MCP endpoint.

A 401 is an answer

A 401 means the server is up and wants credentials. Run the initialize request again with -i and read the WWW-Authenticate header: a server following the authorization spec puts a resource_metadata URL there, which says where to sign in. About a quarter of servers ask for credentials (24% of 1,039 registry addresses, in our census), and the post on MCP OAuth covers what that header should contain.

When curl is not enough

curl proves what your server answers from your machine. It cannot show what a stranger gets, because your machine reaches things the internet does not: localhost, your VPN, a host behind your firewall.

Our MCP connection test sends the same requests from our network instead of yours: the server/discover probe, then the handshake if the server needs it, then tools/list. It reports which revision answered, the tools it listed, or which of the failures above it hit, and saves the result at its own dated URL. For an interactive look at a server you can already reach, the MCP Inspector does the same job with a UI.

Sources

  1. Transports, MCP specification 2025-11-25
  2. Streamable HTTP, MCP specification 2026-07-28
  3. Versioning and Compatibility, MCP specification 2026-07-28