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.

Endpointhttps://usage-lab.pages.dev/mcp
TransportStreamable HTTP (POST JSON-RPC, JSON responses). Stateless: no sessions, GET returns 405.
Protocol versions2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26
AuthYour 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

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

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.