Skip to content
unzoi docs

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

MethodWhat happens
POSTOne JSON-RPC message. A request gets a 200 with the response; a notification gets a 202 with no body.
GET405, with Allow: POST.
DELETE405, 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 initialize once 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.