# MCP server

URL: https://igregulator.io/docs/mcp/
Markdown: https://igregulator.io/docs/mcp.md

> Connect claude.ai, ChatGPT, Claude Code, Claude Desktop, Cursor and other MCP clients to iGregulator — sign in with your account, or use an API key. Verify gambling licences inside the agent, no glue code.

`mcp.igregulator.io` exposes iGregulator as a [Model Context Protocol](https://modelcontextprotocol.io)
server. Compatible clients (Claude Desktop, Cursor, Windsurf, Cline) can
discover and call iGregulator tools without your users writing any
HTTP code.

Two ways in, one plan: **sign in** with your iGregulator account (claude.ai,
ChatGPT and Claude Code connectors — no key to copy), or send the same **API
key** that works against `https://api.igregulator.io`. Either way tool calls
are counted against your account's normal quota (shared by all your keys and
connected apps) — there's no separate MCP plan.

## Why MCP vs. direct API

| | Direct API | MCP |
| --- | --- | --- |
| Where it runs | Your application code | The agent runtime (Claude Desktop, Cursor, etc.) |
| Auth | Your code stores + sends Bearer header | Configured once, agent reuses across sessions |
| Tool discovery | Read OpenAPI, write wrappers | Agent auto-introspects |
| Best for | Server-to-server, batch jobs | End-user agent flows, KYB / compliance assistants |

Use direct API when you build the integration. Use MCP when an agent
*on the user's machine* needs to talk to iGregulator.

## Available tools

Mapped to the public API surface. Tool output is **lean** — a compact
verdict, not the full REST payload — so it stays cheap in your context;
fetch `get_operator` when you need the full dossier.

| Tool | Backs onto | Use case |
| --- | --- | --- |
| `check_domain` | `GET /v1/check` | "Is bet365.com licensed?" (supports `as_of`) |
| `check_domain_batch` | `POST /v1/check/batch` | A KYB sweep — up to 100 domains in one call |
| `search_operators` | `GET /v1/operators/search` | Brand → registered legal entity |
| `get_operator` | `GET /v1/operators/:slug` | Full record incl. licenses + domains (supports `as_of`). Long domain lists are capped — 50 by default, listed links first; `domains_total` says how many there are and `domains_limit` (up to 1,000) asks for more |
| `get_operator_regulatory_actions` | `GET /v1/operators/:slug/regulatory-actions` | Enforcement history (fines, suspensions). An empty list means none is linked to that operator — not a clean record |
| `check_coverage` | `GET /v1/health/coverage` | Data freshness per jurisdiction |
| `list_jurisdictions` | `GET /v1/jurisdictions` | Coverage overview |
| `get_jurisdiction` | `GET /v1/jurisdictions/:code` | One regulator's metadata |
| `get_license` | `GET /v1/licenses/:license_id` | Specific license detail |
| `get_license_history` | `GET /v1/licenses/:license_id/history` | Status-change audit trail |

`check_domain` and each `check_domain_batch` row lead with the API's `verdict`
and `verdict_detail` (see [Reading the answer](https://igregulator.io/docs/mcp/#reading-the-answer)). On a
no-match, they return `match_absence_reason` + `checked_jurisdictions` — so an
agent says "not found in the jurisdictions we cover", never an unqualified
"unlicensed".
On a match, `check_domain` also passes through `verification_url` (the
regulator's own per-domain verification page — Curaçao certificate /
Tobique seal — the citation to surface next to the verdict) and, for
dual-licensed domains, a compact `jurisdictions[]` array so the answer
never flattens a multi-register brand to one jurisdiction.

### Reading the answer

**Branch on `verdict`; quote `verdict_detail`.** Only `licensed` and
`licensed_provisional` mean licensed now — `licensed_provisional` is in force
provisionally (a Curaçao licence under the CGA's final assessment), so say so and
suggest a re-check. Everything else — `licence_not_active`, `domain_not_listed`,
`related_host_listed`, `name_match_only`, `not_found`, `generic_term`, or a value
added later — is "not confirmed as licensed by the registers we cover", never
"unlicensed". The rules behind each value are in
[confidence scoring](https://igregulator.io/docs/confidence/#verdict--the-answer-in-one-field).

`verdict_detail` already names the status in its own words. When you need to
say more about a status, the values are **not interchangeable**, and the server's
own instructions tell the model so on connect:

| `status` | What it means | What an agent should say |
| --- | --- | --- |
| `revoked`, `suspended` | The regulator published an enforcement decision and we read it | That, citing the source |
| `surrendered` | The operator gave the licence up | Not licensed now — not enforcement |
| `expired` | It ran out | Not licensed now |
| `not_in_register` | The register stopped listing it; no reason was published | "No longer listed" — **never** "revoked" |
| `unknown` | Listed, with wording we could not classify | Escalate |

`check_domain`, `check_domain_batch` and `get_operator` return a **lean** result
(they land in the model's context window). A check carries the verdict, `status`,
`status_qualifier`, `domain_status` and — as the API sends them —
`regulator_name`, `status_source_url` (the page that published *this status*,
often not the register itself), `status_observed_at`, `license_id`, and
`related_hosts[]` when the host isn't listed but others on its registrable domain
are (never that host's licence — see
[which host answers](https://igregulator.io/docs/confidence/#which-host-answers--exact-www-then-the-rest-of-the-domain)).
Next to a `verification_url` (a check, or a `get_operator` domain) comes
`verification_page_status`: what the regulator's own page printed about the
licence at our latest read, verbatim, with `verification_page_read_at`. If that
word does not say "in force", the page is not proof (`get_operator`'s summary
names such domains); don't cite the link as confirmation.
`get_operator` gives each licence its `license_id`, `status`, `status_qualifier`
and `license_reference_is_ours` (`true` for Kahnawake, Tobique and the Isle of
Man: `license_number` is then our reference, not the regulator's). Pass a
`license_id` to `get_license` for the full REST record — `status_source_url`,
`status_observed_at`, `not_listed_since`, `last_listed_at` — and to
`get_license_history` for every event with its `note` and the evidence hash
(`snapshot_sha256`). An event with `corrected_at` set was later withdrawn:
never quote it as fact. A `correction_note` without `corrected_at` is a reworded
note on an event that stands, not a withdrawal.

`get_operator_regulatory_actions` says what its list means in the result itself
(`note`, `sources_read`): which regulator publications we read, and that not every
published action is matched to an operator — an empty list is not a clean record.

Webhook + watchlist management (creating endpoints, rotating secrets, etc.) is intentionally not exposed via MCP — those belong in the dashboard at [app.igregulator.io](https://app.igregulator.io). MCP is read-only by design.

## Installation

### Connect from claude.ai, ChatGPT or Claude Code (sign in — no key)

Paste this URL as a custom connector, then sign in with your iGregulator account
when asked:

```
https://mcp.igregulator.io/mcp/account
```

No account yet? The sign-in page has a **sign up** link — free, no card — and
you come straight back to the connection afterwards.

- **claude.ai** (and Claude Desktop, mobile): *Settings → Connectors → Add custom
  connector*, paste the URL, **Add**, then **Connect**. Leave the advanced OAuth
  client fields empty: Claude registers itself.
- **ChatGPT**: turn on developer mode (*Settings → Security and login → Developer
  mode*; availability depends on your plan and workspace), add a new app/connector
  with the URL above, and choose OAuth if asked.
- **Claude Code**:

  ```bash
  claude mcp add --transport http igregulator https://mcp.igregulator.io/mcp/account
  ```

  then run `/mcp` in a session and pick **igregulator → Authenticate**; a browser
  opens for the sign-in.

You will see a consent screen on `app.igregulator.io` naming the app, the site it
returns you to, and what it gets: **read-only licence lookups — all ten tools —
on your plan, counted against your monthly quota, until you revoke it.** It never
sees your password or API keys and cannot change your account. Every app you
connect is listed under [API keys → Connected apps](https://app.igregulator.io/api-keys#connected-apps);
**Revoke** cuts it off at once (its tokens stop working on the next request).

How it works, for the curious: `mcp.igregulator.io/mcp/account` is an OAuth 2.1
protected resource ([RFC 9728 metadata](https://mcp.igregulator.io/.well-known/oauth-protected-resource/mcp/account));
`app.igregulator.io` is its authorization server
([RFC 8414 metadata](https://app.igregulator.io/.well-known/oauth-authorization-server/api/auth)) —
dynamic client registration, PKCE (S256), one scope (`mcp:read`), one-hour access
tokens and rotating refresh tokens, all per the
[MCP authorization spec](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization).
The same endpoint also accepts an API key (`Authorization: Bearer igk_…`).

### Try it without a key

No account needed to see what the server does. With no `Authorization` header, `check_domain`, `search_operators` (top 3), `list_jurisdictions` and `check_coverage` work, 10 requests per hour per IP; the other six tools answer with a pointer to a free key.

```bash
claude mcp add --transport http igregulator https://mcp.igregulator.io/mcp
```

### With a key (all ten tools)

[Create a free account](https://app.igregulator.io/signup) and get an API key at [app.igregulator.io/api-keys](https://app.igregulator.io/api-keys). Founding members get the full Starter plan free — all tools, no card.

### Claude Code

Native HTTP MCP support. One command:

```bash
claude mcp add --transport http igregulator https://mcp.igregulator.io/mcp \
  --header "Authorization: Bearer igk_..."
```

Replace `igk_...` with your real key. Verify with `claude mcp list` — `igregulator` should appear with status `connected`. Restart any active Claude Code session and the tools become available immediately.

### Claude Desktop (recommended path: `mcp-remote` shim)

Claude Desktop's stdio-based MCP support is universal; HTTP support is rolling out. The most reliable config today uses `mcp-remote` to bridge stdio → streamable HTTP. Open `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows) and add:

```json
{
  "mcpServers": {
    "igregulator": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://mcp.igregulator.io/mcp",
        "--header",
        "Authorization: Bearer igk_..."
      ]
    }
  }
}
```

Replace `igk_...` with your real key. Restart Claude Desktop. Verify with: *"What iGregulator tools do you have?"*

### Claude Desktop (native HTTP — newer builds only)

If your Claude Desktop build supports remote MCP servers natively, you can drop the `mcp-remote` shim:

```json
{
  "mcpServers": {
    "igregulator": {
      "url": "https://mcp.igregulator.io/mcp",
      "transport": "streamable-http",
      "headers": {
        "Authorization": "Bearer igk_..."
      }
    }
  }
}
```

If this gives you an "unknown transport" error, your build is too old — fall back to the `mcp-remote` shim above.

### Cursor

Cursor → Settings → Features → Model Context Protocol → Add new MCP server. Use the same `mcp-remote` shim:

```bash
npx -y mcp-remote https://mcp.igregulator.io/mcp --header "Authorization: Bearer igk_..."
```

### Windsurf

Windsurf → Settings → Cascade → Manage MCP Servers. Same `mcp-remote` shim pattern as Cursor; Windsurf documents per-version specifics in their MCP guide.

### Cline / other clients

Anything that speaks MCP can connect. Refer to your client's docs for HTTP/SSE transport configuration; the connect URL is `https://mcp.igregulator.io/mcp` and auth is a bearer header named `Authorization`.

## Example prompts

```
Check whether bet365.com is licensed.
```
→ Agent calls `check_domain` with `domain="bet365.com"`, returns UKGC license details.

```
Find every operator named "Flutter" and show me their licenses.
```
→ `search_operators(q="flutter")` followed by `get_operator(slug=...)` for each result.

```
What's the regulatory action history for license <UUID>?
```
→ `get_license_history(license_id=...)` returns the full status-change timeline.

```
Compare UKGC and MGA — what license types do they each issue?
```
→ `get_jurisdiction(code="UKGC")` + `get_jurisdiction(code="MGA")`.

```
Here are 40 merchant domains — which are licensed?
```
→ one `check_domain_batch(domains=[...])` call, not 40 separate checks.

```
Was virginbet.com licensed on 2026-03-01?
```
→ `check_domain(domain="virginbet.com", as_of="2026-03-01")` — reads `as_of.knowledge` (won't assert a status from before tracking began).

```
Any enforcement actions against Flutter?
```
→ `get_operator_regulatory_actions(slug="flutter-uk-limited")`.

## Authentication

Two credentials work:

- **Signing in** (OAuth 2.1, at `https://mcp.igregulator.io/mcp/account`): what
  claude.ai, ChatGPT and Claude Code connectors do. The MCP server checks each
  access token with `app.igregulator.io` on every request — it must be active,
  issued for this endpoint, and carry `mcp:read` — then calls the API for you
  with its own internal credential; your token is never passed on. A revoked
  connection (Connected apps → Revoke) or a canceled plan stops working on the
  next request. Without a token the endpoint answers `401` with
  `WWW-Authenticate: Bearer resource_metadata="…"`, which is what starts the
  sign-in.
- **An API key**, on either endpoint:

API keys are bearer tokens. The same key works against the direct API and the MCP server. Rotate or revoke at [app.igregulator.io/api-keys](https://app.igregulator.io/api-keys); changes take effect within seconds across both surfaces.

The MCP gateway does not store your key. It checks that the header is shaped `Bearer <key>` and forwards the key on each tool call to the API, which runs the same `requireApiKey` middleware that direct-HTTP callers go through — an unknown or revoked key is refused there, on the first tool call.

A key is optional. Without one, the gateway calls only what the REST API serves without a key (`/v1/check`, `/v1/operators/search`, `/v1/jurisdictions`, `/v1/health/coverage`) and the API applies its public limit to **your** IP, not the gateway's. A header that is present but malformed is still rejected with a 401.

## Rate limits

Without a key: 10 requests per hour per IP — the REST API's own counters, kept per route, not a separate MCP allowance. A keyless tool call counts against the route it calls, from your IP: `check_domain` spends the same `/v1/check` counter as a direct request (ten keyless `check_domain` calls from one IP in an hour use up that IP's `/v1/check` allowance for direct requests too), `search_operators` spends `/v1/operators/search`'s, `list_jurisdictions` `/v1/jurisdictions`'s and `check_coverage` `/v1/health/coverage`'s. With a key, tool calls count against your account's existing quota, one call per tool call. HTTP headers don't cross the MCP boundary: when a limit trips, the tool result is an error whose text is `{ "http_status": 429, …the API's error body… }` — `code`, `details.reason`, and `details.reset_at` where the API sets it — plus, for a keyless caller, a `how_to_fix` hint.

Founding (Starter) keys reach every tool. Only legacy `trial`-tier keys are scoped — to the four tools that also work without a key (`check_domain`, `search_operators`, `list_jurisdictions`, `check_coverage`; 1,000 calls/day) — and return `402 payment_required` with `details.reason=endpoint_requires_paid_plan` on the other tools.

## Discovery

This server publishes a manifest at three URLs (community convention; not a canonical MCP spec field — canonical discovery is the user typing the URL into their client):

- [`https://mcp.igregulator.io/.well-known/mcp.json`](https://mcp.igregulator.io/.well-known/mcp.json)
- [`https://api.igregulator.io/.well-known/mcp.json`](https://api.igregulator.io/.well-known/mcp.json)
- [`https://igregulator.io/.well-known/mcp.json`](https://igregulator.io/.well-known/mcp.json)

All three return the same JSON: server URL, transport, auth flow (key, or OAuth at `/mcp/account`), key-request URL, docs URL.

OAuth discovery for the signed-in endpoint follows the MCP spec: Protected Resource
Metadata at [`/.well-known/oauth-protected-resource/mcp/account`](https://mcp.igregulator.io/.well-known/oauth-protected-resource/mcp/account)
(also at the root `/.well-known/oauth-protected-resource`), naming the authorization
server `https://app.igregulator.io/api/auth`, whose metadata is at
[`/.well-known/oauth-authorization-server/api/auth`](https://app.igregulator.io/.well-known/oauth-authorization-server/api/auth).

## Troubleshooting

**"Tool not found"** — usually the agent never connected. Check that your client logs show iGregulator listed under MCP servers. If it's missing, the bearer header is malformed or the URL is unreachable.

**"Connection failed" or DNS errors** — `curl -I https://mcp.igregulator.io/health` should return `200 OK`. If not, the issue is upstream of MCP.

**Connector says it can't connect, or asks to sign in again** — the sign-in was
revoked under [Connected apps](https://app.igregulator.io/api-keys#connected-apps),
or went unused for 30 days (refresh tokens expire after 30 days without use).
Connect again; you won't be asked to approve an app you already approved unless
you revoked it.

**"402 / plan_inactive" from a signed-in connector** — the account has no active
plan; signed-in connections are refused exactly as its keys would be.

**"401 / api_key_missing / api_key_invalid"** — your key isn't being passed. With `mcp-remote`, double-check the `--header` arg includes `Bearer ` (with the space). Quoting matters in some shells; if your config has issues, restart the client.

**"402 / endpoint_requires_paid_plan"** — a legacy `trial`-tier key hit a paid endpoint. Free founding (Starter) accounts don't see this; create one at [app.igregulator.io/signup](https://app.igregulator.io/signup), or ask the agent to stay on the four keyless tools.

**"429 / rate limited"** — same quota as direct API. Rate-limit headers are not passed through; the API's error body is (`http_status`, `code`, `details.reason`, and `details.reset_at` where set). Branch on `details.reason` as in the [errors reference](https://igregulator.io/docs/errors/): `per_second_limit_exceeded` clears in a second, `monthly_request_limit_reached` at `details.reset_at`, and the keyless `quota_exceeded` at the top of the next UTC hour.

## Feedback

If a tool description or argument schema needs work, write to [founder@igregulator.io](mailto:founder@igregulator.io) — agent-voice quality is something we iterate on.
