Menu

The remote MCP server launch checklist

2026-10-13MCP Checkup

An MCP server that works in your editor has passed one test: your own client, on your own machine. This checklist covers what changes when other people's agents connect over the internet. Each item says why it matters and how to check it.

The commands assume the current revision, 2026-07-28. Where earlier revisions differ, the item says so. Set these once in a bash shell, then paste the commands as they are:

URL=https://your-server.example.com/mcp
H=(-H 'Content-Type: application/json'
   -H 'Accept: application/json, text/event-stream'
   -H 'MCP-Protocol-Version: 2026-07-28')
META='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl-test","version":"0.1"},"io.modelcontextprotocol/clientCapabilities":{}}'
DISCOVER='{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{'"$META"'}}'

If your server speaks 2025-11-25 or earlier, start with an initialize request instead of server/discover, and send back MCP-Protocol-Version and any Mcp-Session-Id it gives you on every later call.

1. Domain and TLS

  • Why it matters: agents connect from machines you don't control. An expired certificate, a hostname the certificate doesn't cover, or a missing intermediate certificate fails there, even when your own browser is happy.
  • How to check: read the verify result and the dates, and make sure the endpoint doesn't redirect to another host, where clients may drop the Authorization header or refuse to follow.
openssl s_client -connect your-server.example.com:443 -servername your-server.example.com </dev/null 2>/dev/null \
  | grep -E 'Verify return code|NotAfter'
curl -sS -o /dev/null -w '%{http_code} redirect=[%{redirect_url}]\n' "$URL" "${H[@]}" \
  -H 'Mcp-Method: server/discover' -d "$DISCOVER"

You want Verify return code: 0 (ok) and an empty redirect=[].

2. Endpoint and transport

  • Why it matters: clients expect one HTTPS URL that accepts POST. That is the Streamable HTTP transport, introduced in 2025-03-26. The older HTTP+SSE transport from 2024-11-05 has been deprecated since then, and 2026-07-28 classifies it as Deprecated under the spec's feature lifecycle policy, so a new server should not adopt it.
  • How to check: publish one exact URL (decide on the trailing slash) and use that same URL in every command here. A server that speaks only 2026-07-28 should answer GET with 405. Under 2025-03-26 to 2025-11-25, GET either opens an SSE stream or returns 405.
curl -sS -o /dev/null -w 'GET %{http_code}\n' -H 'Accept: text/event-stream' "$URL"

3. Accept and Content-Type

  • Why it matters: your server decides per request whether to reply with one JSON object or an SSE stream. So every Streamable HTTP revision makes the client list both application/json and text/event-stream in Accept, and makes your reply's Content-Type one of exactly those two. If your server or a proxy enforces the Accept rule, a client that lists only one type gets 406 Not Acceptable.
  • How to check: a full request succeeds with one of the two content types. A request that lists only application/json gets either a normal reply or a 406 whose body names the missing type, never a bare error page.
curl -sS -D - -o /dev/null "$URL" "${H[@]}" -H 'Mcp-Method: server/discover' -d "$DISCOVER" | grep -i '^content-type'
curl -sS -i "$URL" -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: server/discover' -d "$DISCOVER"

4. Stateless or session

  • Why it matters: under 2026-07-28 there is no handshake and no session. Each request carries its protocol version and client capabilities in _meta, and the server must not rely on earlier requests. Up to 2025-11-25, a server could issue an Mcp-Session-Id at initialize. If you keep sessions for older clients, every instance has to find them, and a session you have ended must get 404, which is what tells a client to start over.
  • How to check: a modern request works as the very first request on a fresh connection, and its response carries no session header.
curl -sS -D - -o /dev/null "$URL" "${H[@]}" -H 'Mcp-Method: server/discover' -d "$DISCOVER" \
  | grep -i '^mcp-session-id' || echo 'no session header'

5. Origin validation

  • Why it matters: browsers attach an Origin header, and DNS rebinding can let a web page reach a server it shouldn't. Every HTTP transport revision says servers must validate Origin. Since 2025-11-25, a request whose Origin is present and invalid must get 403 Forbidden.
  • How to check: send an origin you don't allow and expect 403. With no Origin header (curl sends none by default), the same request should work.
curl -sS -o /dev/null -w '%{http_code}\n' "$URL" "${H[@]}" -H 'Mcp-Method: server/discover' \
  -H 'Origin: https://other-site.example.com' -d "$DISCOVER"

6. Auth discovery

  • Why it matters: authorization is optional in MCP. If you require it, a client with no token has to find out where to get one without reading your docs. Under 2026-07-28 your server must publish OAuth 2.0 Protected Resource Metadata (RFC 9728) that lists at least one authorization server, and must make it findable in at least one of two ways: a resource_metadata URL in the WWW-Authenticate header of its 401, or a well-known URI. (2025-06-18 required the header.) Doing both costs little. Invalid or expired tokens must also get 401.
  • How to check: with no token, expect a 401 carrying WWW-Authenticate: Bearer resource_metadata="...". That URL, or the well-known one, returns JSON with resource and authorization_servers. Clients try the path-specific well-known URI before the root one.
curl -sS -i "$URL" "${H[@]}" -H 'Mcp-Method: server/discover' -d "$DISCOVER" | grep -iE '^HTTP/|^www-authenticate'
curl -sS https://your-server.example.com/.well-known/oauth-protected-resource/mcp
curl -sS https://your-server.example.com/.well-known/oauth-protected-resource

7. Pagination

  • Why it matters: the server chooses the page size. If tools/list returns a nextCursor, the list isn't finished, and a client that stops at page one silently loses the rest. Cursors are opaque, and even an empty string is a valid cursor. An invalid cursor should get error -32602.
  • How to check: follow nextCursor to the end and count the tools. This loop also writes tools.jsonl, which items 8 and 13 use.
cursor=null
while :; do
  body=$(jq -nc --argjson c "$cursor" --argjson m "{$META}" \
    '{jsonrpc:"2.0", id:2, method:"tools/list", params:($m + (if $c == null then {} else {cursor:$c} end))}')
  page=$(curl -sS "$URL" "${H[@]}" -H 'Mcp-Method: tools/list' -d "$body" | sed -n '/^{/p; s/^data: //p')
  echo "$page" | jq -c '.result.tools[]'
  cursor=$(echo "$page" | jq -c '.result.nextCursor // null')
  [ "$cursor" = null ] && break
done > tools.jsonl
wc -l < tools.jsonl

8. Tool descriptions that describe

  • Why it matters: tool names and descriptions go straight into the model's context. Text aimed at the agent, such as "always call this tool first", "ignore other tools" or "don't tell the user", tries to steer it instead of informing it. It is also what a reviewer looking for prompt injection searches for. Describe what the tool does, and let the client decide when to call it.
  • How to check: read every description as the model will see it, then search for orders and for invisible characters. Read anything the first command prints: some matches are harmless, like "never returns null". No output from the second command is what you want.
jq -r '"\(.name): \(.description)"' tools.jsonl \
  | grep -inE 'always|never|ignore|instead of|do not tell|before (any|other)'
jq -r 'select(tostring | test("[\u200b-\u200f\u202a-\u202e\u2060-\u2064\ufeff]")) | .name' tools.jsonl

9. Error shapes

  • Why it matters: agents recover from errors that say what went wrong. MCP separates protocol errors, which are JSON-RPC error objects (an unknown tool is -32602, an unknown method -32601), from tool execution errors, which are normal results with isError: true and a message the model can act on. Under 2026-07-28 the HTTP status matters too: an unknown method gets 404; a missing or mismatched header gets 400 with -32020; an unsupported version gets 400 with -32022 (UnsupportedProtocolVersion) and a list of the versions you support. Clients that speak both eras read these bodies to decide whether your server is modern or legacy, so an empty 400 or 404 can send them down the wrong path.
  • How to check: expect 404 with -32601 from the first command and 400 with -32020 from the second. Then call a real tool with a wrong argument, and expect isError: true with a sentence the model can use.
curl -sS -i "$URL" "${H[@]}" -H 'Mcp-Method: no/such_method' \
  -d '{"jsonrpc":"2.0","id":3,"method":"no/such_method","params":{'"$META"'}}'
curl -sS -i "$URL" "${H[@]}" -H 'Mcp-Method: tools/list' -d "$DISCOVER"

10. Timeouts and long tasks

  • Why it matters: someone is waiting on every tool call. The spec says implementations should set a timeout on every request they send, may reset it when a progress notification arrives, and should still enforce a maximum. Under 2026-07-28, closing a request's response stream cancels the request, and the server should stop the work. Under 2025-11-25, a dropped connection should not be taken as a cancel; clients send notifications/cancelled instead. For work that outlasts a request, the optional Tasks extension returns a handle that the client polls.
  • How to check: call your slowest tool through the public URL with a progress token. Watch whether progress events arrive as they happen, and whether anything between you and the client cuts the stream first. A proxy that buffers holds every event until the end, which is why 2026-07-28 asks servers to send X-Accel-Buffering: no on SSE responses.
time curl -sS -N "$URL" "${H[@]}" -H 'Mcp-Method: tools/call' -H 'Mcp-Name: your_slowest_tool' \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"your_slowest_tool","arguments":{},"_meta":{
        "progressToken":"t1",
        "io.modelcontextprotocol/protocolVersion":"2026-07-28",
        "io.modelcontextprotocol/clientCapabilities":{}}}}'

11. Rate limits

  • Why it matters: the 2026-07-28 tools page lists rate limiting tool calls among the things servers must do. An agent in a retry loop can call the same tool many times in a few seconds.
  • How to check: against your own server only, send a short burst and see what the extra requests get. A clear error, such as 429 with a Retry-After header, lets a client back off. A hang or a 500 does not.
for i in $(seq 1 30); do
  curl -sS -o /dev/null -w '%{http_code} ' "$URL" "${H[@]}" -H 'Mcp-Method: server/discover' -d "$DISCOVER"
done; echo

12. Logs without credentials

  • Why it matters: tokens travel in the Authorization header and sometimes in tool arguments. A request log that captures them keeps working credentials around for anyone who can read the logs.
  • How to check: run this checklist with a test token, then search your logs for it. The count should be zero.
grep -c "$TEST_TOKEN" your-server.log

13. A toolset fingerprint per release

  • Why it matters: people who approve an agent's tools, and the agents themselves, plan around a tool set. A deploy that renames a tool, drops a parameter or changes a schema type breaks those plans, often without a clear error.
  • How to check: hash the names and input schemas after each release, keep the hash with the release, and compare after every deploy. If it changed, you should be able to say which change caused it.
jq -S -s 'map({name, inputSchema}) | sort_by(.name)' tools.jsonl | sha256sum

After launch

Servers drift: certificates expire, proxies get reconfigured, and a dependency upgrade can change an error shape. Run the list again after every deploy, and on a schedule in between.

Paste your address at mcpcheckup.com to see what an agent would see. It connects from outside, the way an agent does, and reports each check in one of four states: verified, failed, observed risk, or not checked with a stated reason. You never get a single score.