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:
401with no key,429over 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
- Connect a client — Claude Code, Cursor, VS Code, Codex, and the rest
- Troubleshooting — zero tools, 403, 405
- MCP tools — the reference