unlob Docs
Browse documentation

MCP

Transports, version negotiation, and what "stateless" costs and buys.

Two transports, one tool layer

https://api.unlob.com/mcp     Streamable HTTP — the hosted service
unlob-mcp                     stdio — a server beside a local index

Both serve the same eleven tools with the same schemas from the same code. A tool behaves identically whether it is reached over a pipe or over the network.

Most people want the HTTP endpoint. Go to Connect a client and pick yours.

Authentication

Your ulb_ key, on every request, in either spelling:

x-api-key: ulb_…
Authorization: Bearer ulb_…

A session never pins an authorization. There is no login step and no session credential to expire — each request carries the key, so the next request on the same session may carry a different one.

Protocol version

Negotiated, not pinned. Offer the version your client speaks in initialize and the server agrees to it if it can. 2025-03-26, 2025-06-18 and 2025-11-25 are all supported.

A server that pins a version and echoes back whatever the client asked for is not negotiating — it is agreeing to something it may not implement. This one does not do that; the version in the initialize result is the version in force.

Stateless, and what that means

The transport runs stateless. The fleet is several replicas with no session affinity, so a session holds no state and no authorization, and any replica can serve any request.

What you get: no sticky routing to arrange, no session to lose when a pod restarts, no recovery path to write.

What you give up: there is no server-initiated event stream and no session to delete. GET and DELETE on /mcp answer 405 with Allow: POST, which is the honest answer — the server is telling you its shape, not failing.

Every tool here is request/response anyway. There is nothing the server would push.

The startup lifecycle

Clients do this for you. To drive it by hand:

curl -sD- https://api.unlob.com/mcp \
  -H "x-api-key: $UNLOB_API_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":"curl","version":"0"}}}'

Note the accept header naming both types. A Streamable HTTP server may answer with either, and a client that names only one is out of spec — and will work right up until the day the server chooses the other.

Then the initialized notification, which is a notification and so gets 202 with an empty body:

curl -sD- https://api.unlob.com/mcp \
  -H "x-api-key: $UNLOB_API_KEY" -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

Then tools/list, then tools/call.

What is billed

Connecting is free. initialize, notifications/initialized, tools/list and ping are not billed — you are not charged for opening a session.

A tool that runs bills one request, the same as the equivalent REST call. See Pricing.

Errors

A refusal arrives in one of two shapes, and which one is deliberate:

  • Transport status for anything that happens before there is a session to speak into: 401 with no key, 429 over the rate limit.
  • Tool error inside the JSON-RPC frame for everything else — a suspended account, an exhausted quota, or a tool that ran and failed.

Billing refusals are tool errors on purpose. A client reads a 402 on an established session as the server having gone away and tears the session down; a billing problem should not present as an outage.

Unroutable requests are true protocol errors: -32601 for an unknown tool, -32602 for bad arguments.

The tools

Eleven, listed at MCP tools, each with its arguments and return schema. They carry outputSchema and return structuredContent, so a client can type the result rather than parsing a string out of a text block.

Every one corresponds to a REST endpoint with the same parameters. A test in the API fails the build if the two ever disagree.

Self-hosting the stdio server

The stdio binary opens the same index shapes the HTTP fleet does — a segmented manifest, a shard tier, a pre-built index directory, or a demo corpus — so it can front a real index locally. It runs unauthenticated by default, since a local server is trusted by whoever spawned it.

Next