Docs

Everything a tool needs to check a license, and what to do with the answer.

Concepts

The verify request

POST https://api.mcplicense.net/v1/mcp/verify
Content-Type: application/json

{"license_key": "mcp-k7m2x9_ABCDE-FGHJK-LMNPQ-RSTUV-WXYZ2", "device_id": "laptop-7f3a"}

No other credentials are needed. Send it when your tool starts, and again whenever the cached answer is older than cache_ttl_seconds.

Responses and reason codes

A valid license answers 200:

{
  "valid": true,
  "reason": "OK",
  "tenant_id": "mcp-k7m2x9",
  "license_id": "9f1c2ab34d5e",
  "seats_used": 1,
  "seats_max": 3,
  "expires_at": "2027-12-31",
  "lease_duration_sec": 86400,
  "lease_expires_at": 1793243644,
  "cache_ttl_seconds": 300
}

Anything else is a refusal with "valid": false, a reason and a message:

HTTPreasonMeaningWhat your tool should do
401INVALID_KEYThe key is malformed or unknown.Stop.
403LICENSE_REVOKEDThe license was revoked.Stop.
403LICENSE_EXPIREDPast its expiry date.Stop.
403SEAT_LIMITEvery seat is held by a device with a live lease.Stop and tell the user.
403TENANT_DISABLEDThe provider (or the platform) has suspended the tenant.Stop.
403GLOBAL_DISABLEDLicensing is switched off platform-wide.Stop.
429RATE_LIMITEDToo many requests for this tenant. The body has retry_after_seconds.Not a denial. Keep running on your last good answer and retry later.

A malformed request (a missing field, say) answers 400 with {"error": {"code": ..., "message": ...}}. A 5xx or a network failure means the check could not be made.

The rule to follow: a 401 or 403 that carries a reason is a decision about the license: stop, and discard anything you cached. Everything else (a network error, a 429, a 5xx, a block page from a proxy) means "could not check", and you decide whether to keep running.

Python example

Standard library only.

import json
import os
import sys
import urllib.error
import urllib.request

API_URL = "https://api.mcplicense.net"


def check_license(license_key, device_id):
    """Return (True, info) for a valid license and (False, info) for a refusal.

    Raises when the check could not be made (network error, 429, 5xx)."""
    request = urllib.request.Request(
        API_URL + "/v1/mcp/verify",
        data=json.dumps({"license_key": license_key, "device_id": device_id}).encode(),
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(request, timeout=5) as response:
            return True, json.load(response)
    except urllib.error.HTTPError as error:
        try:
            body = json.load(error)
        except ValueError:
            body = {}
        if error.code in (401, 403) and body.get("reason"):
            return False, body  # a decision about the license
        raise


ok, info = check_license(os.environ["MCP_LICENSE_KEY"], "my-device-id")
if not ok:
    sys.exit("License check failed ({}): {}".format(info["reason"], info["message"]))
print("License OK")

Node example

Node 18 or later, as an ES module (.mjs).

const API_URL = "https://api.mcplicense.net";

// Resolves to { ok, info }, or throws when the check could not be made.
async function checkLicense(licenseKey, deviceId) {
  const response = await fetch(`${API_URL}/v1/mcp/verify`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ license_key: licenseKey, device_id: deviceId }),
    signal: AbortSignal.timeout(5000),
  });
  const body = await response.json().catch(() => ({}));
  if (response.ok && body.valid) return { ok: true, info: body };
  if ([401, 403].includes(response.status) && body.reason) return { ok: false, info: body };
  throw new Error(`License check could not be made (HTTP ${response.status})`);
}

const { ok, info } = await checkLicense(process.env.MCP_LICENSE_KEY, "my-device-id");
if (!ok) {
  console.error(`License check failed (${info.reason}): ${info.message}`);
  process.exit(2);
}
console.log("License OK");

Caching and outages

You do not need to call the API on every use of your tool.

A ready-made Python helper that does all of this is the single file mcp_auth.py (standard library, Python 3.8+):

import mcp_auth
mcp_auth.require_license()   # reads MCP_LICENSE_KEY; exits with a clear message when not licensed

Set MCP_LICENSE_API_URL=https://api.mcplicense.net, or the API_URL constant in the file.

Managing licenses

You manage your licenses through an HTTPS API. Authenticate with an admin API key in the X-Tenant-Api-Key header. Create a license:

curl -X POST https://api.mcplicense.net/v1/admin/licenses \
  -H "X-Tenant-Api-Key: $MCP_ADMIN_KEY" -H "Content-Type: application/json" \
  -d '{"label": "Jane laptop", "seats_max": 2, "expires_at": "2027-12-31"}'

The response contains the license_key. It is shown once, so hand it to your customer straight away. You can also list licenses, change seats or expiry, revoke or reinstate one, rotate its key, see and free seats, suspend your whole tenant, create more API keys, and read your audit log.

Limits

These defaults apply to every tenant. They protect the service and keep costs predictable. A creation that would pass one answers 403 with LIMIT_REACHED, and GET /v1/admin/tenant shows your limits and how much you use. Need more? Get in touch.

LimitDefault
Verify requests per minute, per tenant100 (more get 429 RATE_LIMITED)
Active licenses50
Seats per license10
Seats across all active licenses200
Active admin API keys5

With the 5-minute cache above, 100 requests a minute covers roughly 500 devices.