Getting started
MCP server
The modes as tools for Claude.ai, Claude Code, Cursor, VS Code and any MCP client — one URL, a key or a sign-in, billed like REST.
The API is also a Model Context Protocol server, so a model can call it as tools instead of you writing the HTTP. The server lives on the same Worker as the API:
POST https://api.directrate.dev/mcp
x-api-key: <your key>
Streamable HTTP, stateless — one JSON-RPC request in, one JSON answer out, no session id, no
server-sent events. Two ways to authenticate: a key from the dashboard, sent as
x-api-key (or Authorization: Bearer <key>); or OAuth sign-in for the clients that cannot send
a header — Claude.ai, Claude Desktop, Claude mobile, ChatGPT — which register themselves and send you
to a consent page that binds the connection to one of your keys.
Install
If your agent has a browser tool, paste this and it installs itself:
Read https://directrate.dev/mcp/install.md and set up the directrate MCP server for me.
Otherwise, each client’s own way in.
Claude.ai, Claude Desktop, ChatGPT — sign in
Settings → Connectors → Add custom connector
Name: directrate
URL: https://api.directrate.dev/mcp
→ Add → Connect → sign in to directrate → choose the API key it may use → Allow
Claude.ai, Claude Desktop and the Claude mobile apps sign in with OAuth: no key to paste. The consent page creates a key for the connector (or you pick one); revoke that key in the dashboard to disconnect. ChatGPT: Settings → Connectors → Create, the same URL, the same sign-in.
The rest send a header. Every snippet reads the key from the environment or a prompt; set
DIRECTRATE_API_KEY first.
Claude Code
claude mcp add --transport http directrate https://api.directrate.dev/mcp --header 'x-api-key: ${DIRECTRATE_API_KEY}'
One command. Keep the single quotes: Claude Code stores the header as written and expands ${DIRECTRATE_API_KEY} each time it starts, so the key never lands in ~/.claude.json. Claude Code can also sign in instead: add the server without --header, then
run /mcp in the session and choose Authenticate; it uses the same consent page over a loopback
redirect, and the key it creates shows in the dashboard under the client’s name.
Cursor
{
"mcpServers": {
"directrate": {
"url": "https://api.directrate.dev/mcp",
"headers": {
"x-api-key": "${env:DIRECTRATE_API_KEY}"
}
}
}
}
~/.cursor/mcp.json, or .cursor/mcp.json in the project. Cursor fills ${env:DIRECTRATE_API_KEY} from the environment it was started in.
VS Code
{
"servers": {
"directrate": {
"type": "http",
"url": "https://api.directrate.dev/mcp",
"headers": {
"x-api-key": "${input:directrate-key}"
}
}
},
"inputs": [
{
"id": "directrate-key",
"type": "promptString",
"description": "directrate API key",
"password": true
}
]
}
.vscode/mcp.json. VS Code asks for the key once and keeps it in its secret store.
Windsurf and other stdio-only clients: the npm package
{
"mcpServers": {
"directrate": {
"command": "npx",
"args": [
"-y",
"@directrate/mcp"
],
"env": {
"DIRECTRATE_API_KEY": "<your key>"
}
}
}
}
For clients that only run local stdio servers (Windsurf, older Cursor, Claude Desktop without a connector): the @directrate/mcp package on npm bridges to the HTTP endpoint and forwards everything unchanged. The key sits in the file's env block (no environment expansion there), so keep the file private. It is a plain bridge — npm install -g @directrate/mcp or npx -y @directrate/mcp (the unscoped directrate-mcp is an alias) — and the tools, schemas and prices are the remote server’s, so it never needs an update when a tool changes. It reads DIRECTRATE_API_KEY; DIRECTRATE_MCP_URL points it at another endpoint (a local gateway), and DIRECTRATE_TOKEN sends an OAuth access token as a bearer instead of a key. Claude Desktop does not need it: it signs in through Settings → Connectors, as Claude.ai does.
From a script
No MCP client, just HTTP: one JSON-RPC document per POST. The transport insists on
Accept: application/json, text/event-stream — both, even though the answer is always JSON — and
Content-Type: application/json; otherwise it answers 406 or 415.
curl -s https://api.directrate.dev/mcp -H "x-api-key: $DIRECTRATE_API_KEY" \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"chains","arguments":{}}}'
The tools
Sixteen tools; each one is a /v1 endpoint, with the endpoint’s request schema as its input schema
(chain, hotel_code, checkin, los, occupancy, currency, max_age — the same names, enums,
defaults and bounds as the reference) and its billing line.
| Tool | Endpoint | What it answers | Credits |
|---|---|---|---|
shop | POST /v1/shop | Every rate plan of one hotel night: public, member, after tax | the source's shop weight (1 credit on most; radisson 3, bestwestern 5, wyndham 6); cached 0.25× |
award | POST /v1/award | The same night in points, with cents-per-point and certificates | as shop |
parity | POST /v1/parity | The direct rate beside Booking.com, Expedia, Hotels.com, Trip.com, Agoda | the direct shop + 1 per channel; +1 when an id was resolved live |
calendar | POST /v1/calendar | Lowest per night over up to 60 nights | 0.1–0.25 a night on priced-calendar chains at los 1; the shop weight per live night elsewhere |
compset | POST /v1/compset | Up to 25 hotels of one chain, lowest each | 0.25–0.5 per hotel returned, minimum 1 a call |
stay | POST /v1/stay | A 2–14 night stay as one vs its nights alone | the shop weight + the calendar nights |
hotels | GET /v1/hotels | The chain's hotel codes around a place | 1 a search (wyndham, bestwestern: the shop weight); a repeat within 24 h 0.25× |
hotel | GET /v1/hotel | Name, address, rating, amenities for one code | 0 from the table; 1 for a live detail fetch |
matrix | POST /v1/matrix | Up to 200 hotels × 366 nights as a batch job | the job's worst case reserved up front; cells from the store at the cache-hit rate |
job | GET /v1/jobs/{id} | A batch job's progress and spend | free |
job_cells | GET /v1/jobs/{id}/cells | A job's cells, paged | free |
watches | GET /v1/watches | The key's watches and what they cost a day | free |
watch_create | POST /v1/watches | Refresh nights on a schedule, webhook on a condition | credits_per_refresh, every refresh, while active |
watch_delete | DELETE /v1/watches/{id} | Stop a watch | free |
chains | GET /v1/chains | Every source with its credit weights and capabilities | free |
usage | GET /v1/usage | Credits used and remaining this month | free |
A result carries a one-line summary first (the lowest public and member rate, the verdict, how many
hotels answered), then the full JSON — also as structuredContent, with credits_charged and
request_id added. Two resources, directrate://chains and directrate://pricing, and two prompts,
shop_hotel and direct_vs_ota, round it out.
Billing is the API’s
A tool call is replayed through the gateway’s own /v1 pipeline: the key middleware, the rate
limit, the quota check and the meter run exactly once per call, and credits_charged on the result
is the number X-Credits-Charged carries on the REST response. No surcharge, no second quota:
REST, the Apify Actor and MCP draw on one
allowance, and /pricing.json prices all three.
The server tells the model the rules at initialize — cached answers at a quarter, errors free,
the quota checked before a call — and every tool’s description states its own price, so a model
can budget before it calls. Reading chains and usage costs nothing.
When it fails
A refused call comes back as a tool error, not a protocol error: isError: true, the
envelope in structuredContent (error.code, error.message, request_id,
http_status) and, on rate_limited, a retry_after_sec the model is told to honour. Bad
arguments are refused by the schema before anything reaches /v1 and cost nothing. A missing or
wrong credential is a 401 with the envelope and the OAuth challenge header. GET /mcp and DELETE /mcp
answer 405: there is no stream to open and no session to end.
A slow call is the one case the transport cannot soften: a calendar of sixty live nights on a chain
without a native calendar, or a large compset, can run past a client’s own timeout (often 60
seconds), and the credits are still spent. Ask for fewer nights, accept max_age, or use matrix,
which answers at once and fills the rest as a job.
How the sign-in works
The endpoint is an OAuth 2.1 resource server and the gateway is its authorization server:
/.well-known/oauth-protected-resource/mcp names it, /.well-known/oauth-authorization-server
describes it, and a client with no credential gets a 401 whose WWW-Authenticate header points
there. The client registers itself (RFC 7591 dynamic registration, public clients), sends you to
/oauth/authorize with PKCE S256, and exchanges the code at /oauth/token.
You sign in with the same magic link or Google as the dashboard, then the consent page asks which API key the connection should act as — a new one named after the client, or an existing one. The access token lives an hour and is refreshed automatically; refresh tokens rotate on every use, and a replayed one revokes the whole grant. Everything the connector reads is billed to that key, and revoking the key in the dashboard disconnects it at once.
Clients that send a static header (Claude Code, Cursor, VS Code) are unaffected: a 401 for a wrong
key is reported as a bad key, not a sign-in.
Disconnecting. Removing the connector in Claude or ChatGPT forgets their tokens but does not revoke the key; revoke it in the dashboard and every token on it stops within about ten seconds. The key shows there under the app’s name with its last use, like any other.
Security
The URL is public by design, like the REST base URL: security is the credential, not the address.
Nothing answers without one — not even tools/list — and every tool call is a /v1 call under the
same key middleware, rate limit and quota, so a caller can do exactly what their key allows and no
more. OAuth tokens are narrower still: one token stands for one key, is bound to this server as its
resource, lives an hour, and refresh tokens rotate on every use (a replayed one revokes the grant).
Only hashes of keys, codes and tokens are stored. The two discovery documents are public by
specification; self-registration is rate-limited per address and a registered client holds nothing
until a signed-in user clicks Allow. Keep the key out of URLs and shared config files — the snippets
above read it from the environment — and if one leaks, revoke it; the quota caps what a leaked key can
spend in the meantime.