Transport
Streamable HTTP at https://api.unzoi.com/mcp, stateless, with the protocol version negotiated per connection.
Most people never need this page — a client is configured with a URL and a header and it works. It is here for
anyone writing a client, debugging one, or wondering why a GET comes back 405.
The handshake
Offer the protocol version you speak; the server answers with the one it picked.
curl -s -X POST https://api.unzoi.com/mcp \
-H "x-api-key: $UNZOI_KEY" \
-H "content-type: application/json" \
-H "accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "my-client", "version": "1.0" }
}
}' {
"jsonrpc": "2.0", "id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": { "tools": {} },
"serverInfo": { "name": "unzoi", "version": "0.1.0" },
"instructions": "Search a global news index: ..."
}
}
This is a real negotiation, not an echo: offer 2024-11-05 and you get 2024-11-05 back, and
offering something the server does not speak gets you what it does. The instructions string is worth
surfacing to your model — it is the one-paragraph description of what this corpus is.
Then send notifications/initialized. It is a notification, so it has no id and the server
answers 202 with an empty body. If your client tries to parse that body as a
JSON-RPC message, it will fail and register none of the tools — this is the single most common way a hand-written
MCP client breaks, on any server.
The verbs
| Method | What happens |
|---|---|
POST | One JSON-RPC message. A request gets a 200 with the response; a notification gets a 202 with no body. |
GET | 405, with Allow: POST. |
DELETE | 405, with Allow: POST. |
GET opens a server-to-client event stream, which this server has no use for: every tool here is
request/response and nothing is ever pushed. DELETE ends a session, and there are no sessions. Both are
permitted to be 405 by the spec, and the Allow header says so rather than leaving a
client to guess.
Stateless, on purpose
There is no session to establish and none to keep alive. No mcp-session-id is issued, and every request
re-resolves its own authorization from its own headers.
That is not a simplification. The endpoint is served by a fleet behind an ingress with no session affinity, so a session pinned to one replica's memory would be found by a client's next request only by luck. Statelessness makes every replica able to answer every request — and it means a leaked session identifier cannot exist, because there is nothing for one to identify.
Practically, for a client author:
- Send the key on every request, not just on
initialize. - There is no reconnect logic to write, and no keepalive.
- You may
initializeonce and then send tool calls indefinitely; nothing expires.
Responses are JSON, not SSE
A tools/call comes back as application/json. The spec permits answering with an SSE
stream instead, and many servers do — which is legal, and unreadable to simple clients and to every
curl in these docs. Send accept: application/json, text/event-stream anyway; some clients
require it and it costs nothing.
The Host header
CORS
GET, POST and DELETE are allowed from any origin, and
mcp-session-id and mcp-protocol-version are both allowed on the request and exposed on
the response. But do not put a key in a browser — there is no origin restriction on a key, so anyone reading your
JavaScript can spend it. Proxy through your own server.
Debugging a client
npx @modelcontextprotocol/inspector
Point it at https://api.unzoi.com/mcp with your key as a header. If it lists six tools, the endpoint is fine and the
problem is in your client's config — most often that it was not restarted after the config changed.