The remote MCP server launch checklist
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
Authorizationheader 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 from2024-11-05has been deprecated since then, and2026-07-28classifies 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-28should answer GET with405. Under2025-03-26to2025-11-25, GET either opens an SSE stream or returns405.
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/jsonandtext/event-streaminAccept, and makes your reply'sContent-Typeone of exactly those two. If your server or a proxy enforces theAcceptrule, a client that lists only one type gets406 Not Acceptable. - How to check: a full request succeeds with one of the two content types. A request that lists only
application/jsongets 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-28there 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 to2025-11-25, a server could issue anMcp-Session-Idatinitialize. If you keep sessions for older clients, every instance has to find them, and a session you have ended must get404, 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
Originheader, and DNS rebinding can let a web page reach a server it shouldn't. Every HTTP transport revision says servers must validateOrigin. Since2025-11-25, a request whoseOriginis present and invalid must get403 Forbidden. - How to check: send an origin you don't allow and expect
403. With noOriginheader (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-28your 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: aresource_metadataURL in theWWW-Authenticateheader of its401, or a well-known URI. (2025-06-18required the header.) Doing both costs little. Invalid or expired tokens must also get401. - How to check: with no token, expect a
401carryingWWW-Authenticate: Bearer resource_metadata="...". That URL, or the well-known one, returns JSON withresourceandauthorization_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/listreturns anextCursor, 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
nextCursorto the end and count the tools. This loop also writestools.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
errorobjects (an unknown tool is-32602, an unknown method-32601), from tool execution errors, which are normal results withisError: trueand a message the model can act on. Under2026-07-28the HTTP status matters too: an unknown method gets404; a missing or mismatched header gets400with-32020; an unsupported version gets400with-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 empty400or404can send them down the wrong path. - How to check: expect
404with-32601from the first command and400with-32020from the second. Then call a real tool with a wrong argument, and expectisError: truewith 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. Under2025-11-25, a dropped connection should not be taken as a cancel; clients sendnotifications/cancelledinstead. 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-28asks servers to sendX-Accel-Buffering: noon 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-28tools 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
429with aRetry-Afterheader, lets a client back off. A hang or a500does 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
Authorizationheader 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.