Usage Lab MCP server
Let an AI agent meter usage for you. The Usage Lab API is also a remote MCP server, so Claude, Cursor, VS Code, the OpenAI Agents SDK and any other MCP client can list and create meters, record usage events with retry-safe idempotency keys, and read monthly totals per customer.
| Endpoint | https://usage-lab.pages.dev/mcp |
|---|---|
| Transport | Streamable HTTP (POST JSON-RPC, JSON responses). Stateless: no sessions, GET returns 405. |
| Protocol versions | 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26 |
| Auth | Your own API key: Authorization: Bearer <key> (or X-Api-Key). Free key at /signup/. Not needed for initialize, tools/list or get_docs. |
| Discovery | /.well-known/ai-catalog.json links the Server Card (application/mcp-server-card+json). Tool metadata for scanners (Smithery-style, built from the live tool list): /.well-known/mcp/server-card.json. |
Tools
get_docs(no key, read-only): Quickstart: key, connect, meters, idempotent events, rollups, links.list_meters(your key, read-only): GET /api/v1/meterscreate_meter(your key, writes, idempotent): POST /api/v1/meters (key, aggregation sum | count | unique_count). meter_exists if it is already there.record_usage_events(your key, writes, idempotent): POST /api/v1/usage with {"events": [...]}, 1-100 events, every event needs its own idempotency_key.get_meter_rollups(your key, read-only): GET /api/v1/meters/{meter}/rollups?month=YYYY-MM&subscription=...get_account(your key, read-only): GET /api/v1/account (plan, limits, event_count).
Every tool is a thin wrapper over the public API, so limits, errors and idempotency rules are the same: batches hold up to 100 events and are all-or-nothing on validation errors; the same key and payload returns duplicate; the same key with a different payload is a conflict and the original is kept. API errors come back as tool results with isError: true and the API's {"error": {"code", "message"}} body, so the agent can read and fix them.
Connect your client
Replace YOUR_API_KEY with your key. Keep it out of shared repos.
Claude Code
Template: replace the placeholders with your own values before running.
claude mcp add --transport http usage-lab https://usage-lab.pages.dev/mcp \
--header "Authorization: Bearer YOUR_API_KEY"
Cursor (~/.cursor/mcp.json or .cursor/mcp.json)
Format reference, not a command.
{
"mcpServers": {
"usage-lab": {
"url": "https://usage-lab.pages.dev/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
VS Code (.vscode/mcp.json, prompts for the key)
Format reference, not a command.
{
"inputs": [
{
"type": "promptString",
"id": "usage-lab-key",
"description": "Usage Lab API key",
"password": true
}
],
"servers": {
"usage-lab": {
"type": "http",
"url": "https://usage-lab.pages.dev/mcp",
"headers": {
"Authorization": "Bearer ${input:usage-lab-key}"
}
}
}
}
Other clients with remote HTTP support
Most clients take a URL plus headers. The transport is called http or streamable-http depending on the client.
Format reference, not a command.
{
"mcpServers": {
"usage-lab": {
"type": "streamable-http",
"url": "https://usage-lab.pages.dev/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
Clients that only run local (stdio) servers, such as Claude Desktop config files
Bridge with the community mcp-remote package, which forwards the header:
Format reference, not a command.
{
"mcpServers": {
"usage-lab": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://usage-lab.pages.dev/mcp",
"--header",
"Authorization:${USAGE_LAB_AUTH}"
],
"env": {
"USAGE_LAB_AUTH": "Bearer YOUR_API_KEY"
}
}
}
}
OpenAI Agents SDK (Python)
Fragment: needs pip install openai-agents and OPENAI_API_KEY, so our docs test does not run it.
import asyncio, os
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp
async def main():
async with MCPServerStreamableHttp(name="usage-lab", params={
"url": "https://usage-lab.pages.dev/mcp",
"headers": {"Authorization": f"Bearer {os.environ['USAGE_LAB_KEY']}"},
}) as usage_lab:
agent = Agent(name="billing-helper", instructions="Use Usage Lab tools to answer usage questions.", mcp_servers=[usage_lab])
result = await Runner.run(agent, "How many agent_tool_calls did cus_mcp_demo use this month?")
print(result.final_output)
asyncio.run(main())
Try it with curl
No key needed for the handshake, the tool list and get_docs:
curl -s https://usage-lab.pages.dev/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl-test","version":"1.0"}}}'
echo
curl -s https://usage-lab.pages.dev/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | python3 -c 'import json,sys; print([t["name"] for t in json.load(sys.stdin)["result"]["tools"]])'
Example response, not a command.
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{"listChanged":false}},"serverInfo":{"name":"usage-lab","title":"Usage Lab","version":"0.1.0",...},"instructions":"Usage Lab meters usage for usage-based billing. Typical flow: ..."}}
['get_docs', 'list_meters', 'create_meter', 'record_usage_events', 'get_meter_rollups', 'get_account']
curl -s https://usage-lab.pages.dev/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_docs","arguments":{}}}' | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["structuredContent"]["docs"][:300])'
With your key: create the meter, record two agent tool calls, send the same call again (a retry), then read the rollup. The retry comes back as duplicate, so the customer is billed for 2 calls, not 4.
# create the meter once (meter_exists is fine), record two tool calls, retry them, read the rollup
call() { curl -s https://usage-lab.pages.dev/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -H "Authorization: Bearer YOUR_API_KEY" -d "$1"; echo; }
call '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"create_meter","arguments":{"key":"agent_tool_calls","aggregation":"count"}}}'
EVENTS='{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"record_usage_events","arguments":{"events":[
{"meter":"agent_tool_calls","subscription":"cus_mcp_demo","idempotency_key":"mcp-docs:run1:call_1","properties":{"traffic_class":"ai_agent","tool":"search"}},
{"meter":"agent_tool_calls","subscription":"cus_mcp_demo","idempotency_key":"mcp-docs:run1:call_2","properties":{"traffic_class":"ai_agent","tool":"fetch"}}]}}}'
call "$EVENTS" | python3 -c 'import json,sys; print("first try", json.load(sys.stdin)["result"]["structuredContent"]["summary"])'
call "$EVENTS" | python3 -c 'import json,sys; print("retry ", json.load(sys.stdin)["result"]["structuredContent"]["summary"])'
call '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_meter_rollups","arguments":{"meter":"agent_tool_calls","subscription":"cus_mcp_demo"}}}' \
| python3 -c 'import json,sys; r=json.load(sys.stdin)["result"]["structuredContent"]; print(r["month"], r["rollups"])'
Example response, not a command.
{"jsonrpc":"2.0","id":1,"result":{"content":[...],"structuredContent":{"http_status":201,"meter":{"key":"agent_tool_calls","aggregation":"count",...}},"isError":false}}
first try {'created': 2, 'duplicate': 0, 'conflict': 0}
retry {'created': 0, 'duplicate': 2, 'conflict': 0}
2026-10 [{'subscription': 'cus_mcp_demo', 'quantity': 2, 'event_count': 2, 'exported_quantity': 0, 'pending_export_quantity': 2, 'exported_at': None}]
If the account already has agent_tool_calls, the first call returns a meter_exists tool error, which is fine. If you ran this before, both tries say duplicate. That is the idempotency key working.
Protocol notes for client authors
- 2026-07-28 requests carry
_meta["io.modelcontextprotocol/protocolVersion"]and theMCP-Protocol-Version,Mcp-Methodand (for tools/call)Mcp-Nameheaders; mismatches return 400 with-32020, an unknown version returns 400 with-32022and the supported list.server/discoveris implemented. - Older clients send
initialize; the server answers with the requested version if it supports it (otherwise 2025-11-25) and mints noMcp-Session-Id, so every POST stands alone. Notifications get 202. - CORS:
Access-Control-Allow-Origin: *; OPTIONS returns 204 and allowsAuthorization,Mcp-Session-Id,Mcp-Protocol-Version,Mcp-MethodandMcp-Name, so browser-based agents work. - Tool annotations mark read-only tools, and the two write tools as idempotent and non-destructive. There is no delete tool: deleting an account stays in the API (
DELETE /api/v1/account) and needs an explicit confirmation.
Related: metering AI agent and MCP tool calls per customer, idempotency keys, OpenAPI spec.
FAQ
Does the MCP server store my API key?
No. The key travels in the Authorization header of each MCP request and is forwarded only to the Usage Lab API for that one call. The server is stateless and keeps no sessions, logs or keys.
Can I use it without an API key?
Yes for discovery: initialize, tools/list and the get_docs tool work without a key, so registries and agents can inspect it. Tools that read or write your account return a clear unauthorized tool error until you add your own key (free at /signup/).
Why does record_usage_events require an idempotency key on every event?
Agents retry. With a key built from something a retry repeats (run id + step + tool call id), a retried call comes back as duplicate and is counted once. The server never makes a key up, because a random key would bill every retry.
Which MCP protocol versions are supported?
2026-07-28 (stateless, server/discover, per-request _meta) and the handshake versions 2025-11-25, 2025-06-18 and 2025-03-26 (initialize). It answers with JSON, never opens a GET stream (405), and mints no Mcp-Session-Id.
Are AI agents and bots allowed?
Yes. Agents and bots are the point. There is no User-Agent filtering, and their API calls count as real usage.
- Playground: validate a usage event in your browser
- Usage event validator: check event JSON and idempotency keys
- Drop-in LLM metering helper + margin calculator
- Usage-based pricing calculator (LLM tokens or events)
- Live end-to-end demo
- API docs
- Quickstart: meter an AI app
- Meter Vercel AI SDK, OpenAI and Anthropic tokens (input + output)
- Meter OpenAI Agents SDK tool calls and tokens per customer
- Code examples
- Usage metering compared: Usage Lab vs Stripe meters, Orb, Lago, Metronome, OpenMeter
- OpenMeter alternative: Usage Lab vs OpenMeter (Kong)
- Lago alternative: Usage Lab vs Lago
- Amberflo alternative: Usage Lab vs Amberflo
- Metering usage for Stripe usage-based billing
- Deduping usage events with idempotency keys
- Backfilling late usage events: replays, late windows and adjustments
- Usage Lab vs UsageBox: an honest comparison
- Metronome and Orb alternatives for small SaaS teams
- How to meter LLM token usage per customer for billing
- How to meter AI agent and MCP tool calls per customer
- Stripe Billing Meters vs a usage ledger: when you need both