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.
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, thennotifications/initialized, thentools/list, carrying theMcp-Session-Idthe first response handed you [1]. - On 2026-07-28 there is no handshake. Each request carries its version in
_metaand in theMCP-Protocol-VersionandMcp-Methodheaders [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
resultwithsupportedVersions - an error with code
-32020,-32021or-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.