AgenticWebScore
Scan

Learn

Testing an MCP server with curl: initialize, the initialized notification and tools/list

Use curl as an MCP server tester: the commands that open a Streamable HTTP session and list tools, the 405, 400 and 401 answers, and curl against MCP Inspector.

7 min read 1644 words

  • MCP
  • Streamable HTTP
  • curl
  • MCP Inspector
  • testing

An MCP server tester has to send 3 requests, and curl can send all of them. They are initialize, the notifications/initialized notification and tools/list, each a POST to the server's endpoint. Each POST carries Accept: application/json, text/event-stream. Each one after the first also carries MCP-Protocol-Version, and the Mcp-Session-Id the server issued, if it issued one. That is the handshake of MCP revision 2025-11-25, and a later section shows the request a 2026-07-28 server expects instead.

Which MCP revision you are testing

The current revision is 2026-07-28, according to the MCP versioning page. Its changelog removes the initialize handshake and protocol-level sessions. Every request now states its version in _meta, and every server must answer server/discover.

The 2 MCP servers in AgenticWebScore's scan records still speak the older revision. Cloudflare's documentation server answered initialize with 2025-11-25 in its scan record of 20 October 2026. This site's own server did the same on 25 October 2026.

The 2026-07-28 compatibility rules let one server speak both revisions. They ask a server that speaks only 2026-07-28 to answer initialize with an error that names its versions. So start with the handshake, and read what comes back.

Step 1: send initialize

The client opens the session with initialize: the version it wants, its capabilities, and its name. Save the response headers to a file, because a session id arrives in a header.

MCP=https://example.com/mcp
curl -sS -D headers.txt -X POST "$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-test","version":"1.0"}}}'

This site's server at https://agenticwebscore.com/mcp answers with one JSON object, trimmed here and broken into lines:

{"jsonrpc":"2.0","id":1,"result":{
  "protocolVersion":"2025-11-25",
  "capabilities":{"tools":{"listChanged":false}},
  "serverInfo":{"name":"com.agenticwebscore/scan","title":"AgenticWebScore","version":"0.3.0"},
  "instructions":"Call scan_site with a domain to measure it, …"}}

protocolVersion is the version the server chose. The lifecycle has it repeat yours when it supports it, and name another one when it does not. A tools key under capabilities means the server has tools to list. Then read the session id, if there is one:

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

When a server issues an Mcp-Session-Id

Under the 2025-11-25 transport, a server may assign a session id in an Mcp-Session-Id header on its answer to initialize. When it does, the client must send that header on every later request. A server that requires it should answer 400 to a request without it. Once the server ends the session, it answers 404, and the client starts over with a new initialize.

A stateless server issues no id. This site's server is one, so headers.txt has no such line and $SID stays empty. curl sends no header at all for -H "Mcp-Session-Id: $SID" when $SID is empty, so the commands below work for both kinds of server.

Step 2: send the initialized notification

After a successful initialize, the client must send notifications/initialized. It is a notification, so it has no id and gets no answer body.

curl -sS -i -X POST "$MCP" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -H "Mcp-Session-Id: $SID" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
HTTP/2 202
cache-control: no-store

The transport requires 202 Accepted with no body for a notification the server accepts. Send the version the server chose in step 1, not the one you asked for. A server that receives a version it does not support must answer 400 Bad Request.

Step 3: list the tools

curl -sS -X POST "$MCP" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -H "Mcp-Session-Id: $SID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
  | jq -r '.result.tools[].name'
scan_site
get_score
list_checks
get_report

Each tool in the full answer has a name, a description and an inputSchema. A nextCursor in the result means the server has another page of tools. Stop here when you only want to test the server: tools/call runs a tool, and a tool can change something.

A JSON answer or an event stream

For a request, the transport lets the server answer with Content-Type: application/json or with Content-Type: text/event-stream, and a client must accept both. This site's server always answers JSON.

A server that streams sends Server-Sent Events, and the answer is the data: line whose JSON carries your request's id. The 2025-11-25 transport also asks the server to open with an event that has an id and an empty data field. A stream has this shape:

id: 1
data:

data: {"jsonrpc":"2.0","id":2,"result":{"tools":[...]}}

Add -N so curl prints each event as it arrives, and cut the JSON out with sed:

curl -sS -N --max-time 15 -X POST "$MCP" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -H "Mcp-Session-Id: $SID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
  | sed -n 's/^data: //p' | jq .

A stream may stay open after the answer, which is why the command has --max-time.

Common errors and what they mean

Common errors and what they mean — Answer, What it usually means
Answer What it usually means
405 to a GET Normal: the server offers no stream on GET.
405 to a POST The wrong URL, or a server on the old HTTP+SSE transport.
400 An unsupported MCP-Protocol-Version, a missing session id, or a 2026-07-28 server refusing initialize.
401 with resource_metadata The server needs OAuth, and the URL names its metadata.
404 with a session id The session ended. Send initialize again.
403 The server rejected your Origin header.
200 with a JSON-RPC error The transport worked. The method or its params did not.

A 405 to a GET. The 2025-11-25 transport has a server answer a GET with an event stream or with 405 Method Not Allowed. A browser visit is a GET, so a 405 shows only that something answers. This site's endpoint:

curl -sS -i https://agenticwebscore.com/mcp
HTTP/2 405
content-type: application/json; charset=utf-8
allow: POST
cache-control: no-store

{"error":"method_not_allowed","message":"Use POST with one JSON-RPC 2.0 message as application/json. This server offers no stream on GET."}

A 400 for a version. Sent MCP-Protocol-Version: 2024-01-01, this site's server answers 400 with the versions it speaks:

{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Unsupported MCP-Protocol-Version. This server speaks 2025-11-25, 2025-06-18."}}

A 401 with resource_metadata. MCP authorization is optional. A server that uses it must tell clients where its OAuth protected resource metadata is, for example in a resource_metadata parameter on the WWW-Authenticate header of its 401 (RFC 9728 §5.1). The specification's example:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         scope="files:read"

Fetch that URL to find the authorization server. A 401 with no challenge only says the server refused you.

A JSON-RPC error inside a 200. Asked for server/discover, the 2026-07-28 method, this site's 2025-11-25 server answers HTTP 200 with error -32601, “Method not found”. The request reached the server, and the server does not know the method.

The same test on a 2026-07-28 server

A 2026-07-28 server needs no handshake. Each POST carries the version twice, in the MCP-Protocol-Version header and in _meta. It also names its method in an Mcp-Method header, as the 2026-07-28 transport requires:

curl -sS -X POST "$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-test","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'

A header that does not match the body gets 400 with error -32020. A version the server does not speak gets 400 with -32022, and the error lists the versions it supports. A 400 with neither error in its body comes from a legacy server: go back to step 1. Both servers in AgenticWebScore's stored scan records chose 2025-11-25, so no output is shown for this command.

curl, MCP Inspector or an online tester

All 3 send the same requests, and each shows something the others hide.

curl, MCP Inspector or an online tester — , curl, MCP Inspector, Online tester
curl MCP Inspector Online tester
Shows The status, headers and body of each request, raw The JSON-RPC transcript, the HTTP layer, and a form for each tool What the vendor's page prints
Reaches Anything your machine reaches Anything your machine reaches Public URLs, if its servers send the requests
Use it when You need to read a 400, 401 or 405, or run a check in CI You want to call a tool, or test a server on your laptop You want a quick look at a public endpoint

curl follows no protocol for you. You write each request and read each answer, and that is why it explains a bare 405 or 400 better than the other two. It runs in CI or over SSH with nothing to install.

The MCP Inspector is the project's reference tester. npx @modelcontextprotocol/inspector starts a web client and needs Node 22.19.0 or newer. Its Tools tab builds a form from each input schema, its Protocol tab shows the JSON-RPC transcript, and its Network tab shows status, headers and bodies. With --cli, --method tools/list prints the tool list from a script. A Protocol Era setting picks the initialize handshake (the default), auto, or modern for 2026-07-28. A call from the Tools tab runs the tool, so point it at a test server.

An online tester is a hosted page. If its servers send the requests, it cannot reach a server on your own machine or behind a VPN, and its operator sees your endpoint and any token you paste. If your browser sends them, your server needs CORS headers. Check which revision the tester speaks before you trust a failure: one that sends only the 2025-11-25 handshake, as the scan's check does, reports a 2026-07-28 server as failing.

What AgenticWebScore's MCP server tester sends

To run it on your own site, use the MCP server tester.

The MCP server check is the scanner's MCP server tester. It runs steps 1 to 3 and nothing else. It finds the endpoint in a server card, in the AI catalog at /.well-known/ai-catalog.json, or in a homepage link whose relation is mcp or mcp-server. Failing those, it tries /mcp, which counts when a GET gets 405 and Allow: POST. It opens an endpoint only on the scanned site's host or one of its subdomains, on the same port, and never one on another site.

The session is at most 3 POSTs within 12 seconds. It offers 2025-11-25, accepts the version the server chooses, and echoes Mcp-Session-Id when one is issued. It reads up to 64 KB of each answer, JSON or event stream. It never sends tools/call, prompts/get or resources/read, so the scan calls no tool, and it sends no credentials.

The check passes when tools/list names at least 1 tool. A 401 with a challenge, or a server with no tools, is a partial pass, and neither holds level 4. One limit: the check sends only the 2025-11-25 handshake, so a server that speaks only 2026-07-28 shows up as a failed handshake. Cloudflare's record names 2 tools, and this site's record names 4.

How an agent learns the endpoint before step 1 is the subject of MCP server cards. Tools, not pages covers what to put in a tool declaration. To see the 3 requests and their status codes for your own server, scan your site.