# Connect an AI assistant with the MCP server

HostTracker runs a hosted **Model Context Protocol (MCP) server**. Connect it to an AI assistant and the assistant
can operate your account from a conversation: run a live check, list what is down, pause a monitor, schedule
maintenance, publish a status page incident or wire up a webhook. Every tool is a thin wrapper over the
[REST API v2](/integrations/rest-api/), so it can do exactly what your credential allows and nothing more.

| | |
|---|---|
| Endpoint | `https://mcp.host-tracker.com/mcp` |
| Transport | Streamable HTTP. Nothing to install or run locally. |
| Sign-in | **OAuth 2.1** (sign in and approve, recommended) or an `Authorization: Bearer <token>` header with an [API token](/integrations/api-authentication/) |
| Plan | Your plan must include API access - see [Rate limits and quotas](/integrations/rate-limits/#which-plans-include-the-api) |
| Request limit | 60 requests per minute per client IP address on the MCP endpoint, plus your plan's API quota |
| Paging | List tools take `limit` (1-50, default 20) and `cursor` |
| Time | Every timestamp in and out is Unix seconds |

## Connect with OAuth (recommended)

With OAuth there is no token to copy. The client opens HostTracker's sign-in page, then a consent card that lists
exactly what the assistant will be allowed to do. You press **Approve** and the client handles the rest: access
tokens are short-lived (about an hour) and refreshed silently.

- If the client does not ask for specific scopes, the connection gets `check` + `monitor:read` (run instant checks,
  read monitors, uptime and incidents).
- The `account` scope family is never offered to connected apps.
- To widen an OAuth connection later, remove and re-add the connector and approve the wider set - scopes on an
  existing connection cannot be widened silently.
- To cut a connection off, open **Integrations -> API -> Connected apps** and click **Revoke**. Its refresh chain
  dies immediately; the last access token expires within the hour.

**Claude.ai and Claude Desktop:** Settings -> Connectors -> **Add custom connector** -> paste
`https://mcp.host-tracker.com/mcp` -> **Connect**. Sign in if asked, press **Approve** on the consent card, and the
connector shows as connected.

**Claude Code:**

```sh
claude mcp add --transport http hosttracker https://mcp.host-tracker.com/mcp
```

Then run `/mcp`, pick `hosttracker` and choose **Authenticate**. The browser opens the sign-in and consent page;
Claude Code listens on a localhost port for the redirect, which is normal.

**ChatGPT:** in developer mode, open connectors and add `https://mcp.host-tracker.com/mcp`. ChatGPT shows its "link
account" step after the first call, which opens the same sign-in and consent page.

**Any other client** with an OAuth-capable connector dialog needs only the URL.

## Connect with an API token

Use a token for clients that only send static headers, for CI, or when you want a hand-picked scope set and
expiration.

1. Open **Integrations -> API** in the app (`/integrations/api`) and create a token.
2. Tick only the scopes the assistant needs (table below). Scope leaves do not imply each other: `monitor:write`
   does not grant `monitor:read`. A bare family name (`monitor`) covers every leaf in it.
3. Set a short expiration and, if the client runs from a fixed address, an IP allow-list. A token cannot be revoked
   before it expires.
4. Copy the token - it is shown once.

| You want the assistant to... | Scopes |
|---|---|
| Run instant checks | `check` |
| See monitors, uptime, results and incidents | `monitor:read` |
| Create, edit, pause or delete monitors and maintenance windows; comment incidents | `monitor:write` |
| See who is notified about what | `contact:read`, `subs:read` |
| Manage contacts and contact groups | `contact:write` (plus `contact:read` to list them) |
| Subscribe or unsubscribe contacts to monitors | `monitor:write` |
| Manage webhooks | `webhook:read`, `webhook:write` |
| Manage status pages and publish incidents on them | `statuspage:read`, `statuspage:write` |
| Read account usage, limits and quota | `account:read` (API tokens only - OAuth connections never get it) |

There is no reason to grant `account:write`: the MCP server refuses every write under `/account` whatever the token
allows.

### Claude Code

Project-scoped, in `.mcp.json` at the repository root:

```json
{
  "mcpServers": {
    "hosttracker": {
      "type": "http",
      "url": "https://mcp.host-tracker.com/mcp",
      "headers": { "Authorization": "Bearer YOUR_HOSTTRACKER_API_TOKEN" }
    }
  }
}
```

Or from the command line, which writes the same entry:

```sh
claude mcp add --transport http hosttracker https://mcp.host-tracker.com/mcp \
  --header "Authorization: Bearer YOUR_HOSTTRACKER_API_TOKEN"
```

Check it with `claude mcp list`. Inside a session the tools appear prefixed, for example
`hosttracker_run_instant_check`.

### Claude Desktop with a token

The Desktop connector dialog is OAuth-only, so a token goes through the `mcp-remote` bridge. Settings -> Developer
-> **Edit config** opens `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "hosttracker": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://mcp.host-tracker.com/mcp",
               "--header", "Authorization:${HT_AUTH}"],
      "env": { "HT_AUTH": "Bearer YOUR_HOSTTRACKER_API_TOKEN" }
    }
  }
}
```

Quit and restart Desktop completely. Node.js 18 or newer must be on the PATH the desktop app sees.

### Cursor

`~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project):

```json
{
  "mcpServers": {
    "hosttracker": {
      "url": "https://mcp.host-tracker.com/mcp",
      "headers": { "Authorization": "Bearer YOUR_HOSTTRACKER_API_TOKEN" }
    }
  }
}
```

### VS Code (GitHub Copilot agent mode)

`.vscode/mcp.json` in the workspace. The `inputs` block keeps the token out of the file - VS Code prompts for it once
and stores it in its secret storage:

```json
{
  "inputs": [
    { "type": "promptString", "id": "ht-token", "description": "HostTracker API token", "password": true }
  ],
  "servers": {
    "hosttracker": {
      "type": "http",
      "url": "https://mcp.host-tracker.com/mcp",
      "headers": { "Authorization": "Bearer ${input:ht-token}" }
    }
  }
}
```

### Windsurf

`~/.codeium/windsurf/mcp_config.json`:

```json
{
  "mcpServers": {
    "hosttracker": {
      "serverUrl": "https://mcp.host-tracker.com/mcp",
      "headers": { "Authorization": "Bearer YOUR_HOSTTRACKER_API_TOKEN" }
    }
  }
}
```

Some Windsurf versions use `url` instead of `serverUrl`; if the entry is ignored, try the other key.

### Any other client

Any client that can dial a streamable-HTTP MCP endpoint and send a static header works:

```
URL     https://mcp.host-tracker.com/mcp
Header  Authorization: Bearer YOUR_HOSTTRACKER_API_TOKEN
```

A client that can launch a command but not send headers can use the bridge:

```sh
npx -y mcp-remote https://mcp.host-tracker.com/mcp --header "Authorization:${HT_AUTH}"
```

with `HT_AUTH` set to `Bearer YOUR_HOSTTRACKER_API_TOKEN` in the environment.

## Verify the connection

This handshake needs no token and proves the endpoint is reachable from your machine:

```sh
curl -sS https://mcp.host-tracker.com/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-06-18","capabilities":{},
                 "clientInfo":{"name":"curl","version":"1"}}}'
```

The reply is a `text/event-stream` frame whose `data` line carries `serverInfo`. Then ask the assistant: "use
hosttracker to run an http check on example.com".

## The tools

65 tools: 63 curated ones (one API call each, small argument sets) plus a guarded generic door for everything else.
The scope is what your token or OAuth connection needs.

### How arguments are passed

The tools take flat, simple arguments. Three shapes matter, and getting them wrong is the most common reason a call
fails:

- **Lists are comma-separated strings, not JSON arrays.** `tags="prod,eu"`, `monitorIds="ID1,ID2"`,
  `alertTypes="down,up"`, `events="monitor.down,monitor.up"`, `componentIds="C1,C2"`. Passing `["a","b"]` to one of
  these is refused by the client or the server before anything runs.
- **Arguments whose name ends in `Json` take a JSON document as a string.** `settingsJson="{\"keywords\":\"Checkout\"}"`,
  `itemsJson="[{\"contact\":\"ID\",\"events\":[\"down\"]}]"`. Inside that string the members are exactly the REST
  API's members.
- **Timestamps are Unix seconds** (integers) and **intervals are seconds**.

A tool has only the arguments listed below. No tool takes an `items` or a `scope` argument, and none takes a raw
JSON array or object: where the REST body has one, the tool either takes a `...Json` string or builds it for you
from comma-separated arguments (`create_webhook` builds `scope` from `tags` or `monitorIds`). Anything a tool does
not expose is reachable through `api_request`.

List tools take `limit` 1-50 (default 20; out-of-range values are clamped, not refused) and `cursor` from the
previous answer. Keep calling with the cursor until the answer has no continuation line.

### Instant checks

| Tool | What it does | Scope |
|---|---|---|
| `run_instant_check` | Runs a one-off check from HostTracker's locations, polls up to about 30 s and returns per-location results plus the public result-page URL. A check still running comes back partial with the `dbId` and `id` to poll. | `check` |
| `get_check_result` | Fetches the current results of an instant check. | `check` |
| `list_check_types` | Lists the instant-check types and the device profiles a page-load (`waterfall`) check can emulate. | none |

**`run_instant_check`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `url` | string | yes | The site or host, e.g. `example.com` or `https://example.com`. |
| `type` | string | no (default `http`) | A type token from `list_check_types`: `http`, `ping`, `port`, `trace`, `dns`, `dnsbl`, `whois`, `webRisk`, `crawl`, `waterfall` (`pageSpeed` is accepted as an alias). |
| `pools` | string, comma-separated | no (default everywhere) | Pool ids to run from, e.g. `westeurope,northamerica`. An unknown id is refused with the valid ones. (The tool's own example says `europe`; there is no `europe` pool - see [Location pool ids](#location-pool-ids).) |
| `device` | string | no | Device-emulation profile for a `waterfall` check, from `list_check_types`. |
| `strictTls` | boolean | no (default `false`) | `http` only: fail the check on an untrusted, incomplete, mismatched or self-signed certificate. |

**`get_check_result`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `dbId` | integer | yes | The `dbId` `run_instant_check` returned. |
| `id` | string (GUID) | yes | The check id `run_instant_check` returned. |

`list_check_types` takes no arguments.

### Monitors

| Tool | What it does | Scope |
|---|---|---|
| `list_monitors` | Lists monitors; filters combine with AND. | `monitor:read` |
| `get_monitor` | Reads one monitor with its full configuration. | `monitor:read` |
| `create_monitor` | Creates one monitor (`POST /monitor`). Adds no subscriptions: attach contacts afterwards with `subscribe_contact`. | `monitor:write` |
| `update_monitor` | Changes only the arguments you pass (`PATCH /monitor/{id}`). | `monitor:write` |
| `delete_monitor` | Deletes one monitor and its subscriptions. Preview first, then `confirmed=true`. | `monitor:write` |
| `pause_monitor` | Stops checking and alerting until resumed (sets `enabled: false`). | `monitor:write` |
| `resume_monitor` | Resumes a paused monitor (sets `enabled: true`). | `monitor:write` |
| `copy_monitor` | Copies a monitor to one or more new addresses. Many addresses answer with a job id. | `monitor:write` |
| `bulk_create_monitors` | Validates a batch; with `submit=true` creates it as an asynchronous job. | `monitor:write` |
| `bulk_update_monitors` | Applies one patch to every monitor a filter selects; validates unless `submit=true`. | `monitor:write` |
| `bulk_delete_monitors` | Deletes every monitor a filter selects; validates unless `confirmed=true` and `expectedCount` are both sent. | `monitor:write` |
| `list_monitor_types` | Lists every monitor type with its label, minimum interval and whether your plan can create it. | none (a token adds your limits) |

:::caution[Pass intervals in seconds]
The API reads `interval` in **seconds**, and `create_monitor`, `update_monitor` and the bulk tools pass the value
through unchanged. The tools' own parameter text says minutes (and a bulk example shows `{"interval":5}`), but the
value is read as seconds: send `300` for five minutes, `60` for one minute - also in bulk defaults and bulk
patches. A value that is not one of the account's allowed intervals is refused with `422 invalid_interval`
(`get_account` lists them under `limits.intervals`).
:::

**`list_monitors`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `q` | string | no | Free-text search over name and url. |
| `state` | string, comma-separated | no | `up`, `down`, `paused`, `maintenance`. |
| `type` | string, comma-separated | no | Monitor type tokens, e.g. `http,ping`. |
| `tag` | string, comma-separated | no | Tags; a monitor matches when it carries any of them. |
| `id` | string, comma-separated | no | Monitor ids. |
| `sort` | string | no | `name`, `state`, `type`, `interval`, `lastChange`, `url`, `tags` or `created`; add `:desc` or `:asc`, e.g. `lastChange:desc`. |
| `limit`, `cursor` | integer, string | no | Paging (1-50, default 20). |

**`get_monitor`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `id` | string | yes | The monitor id. |
| `expand` | string, comma-separated | no (default `settings,uptime`) | Extra blocks: `settings`, `uptime`, `lastResult`, `lastIncident`, `subscription`, `maintenance`, `attached`, `spans`. |
| `from`, `to` | integer (Unix seconds) | no | Window for `uptime` and `spans`. |

**`create_monitor`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `type` | string | yes | Monitor type token: `http`, `api`, `ping`, `port`, `cntCheck`, `waterfall`, `tran`, `sslExp`, `domainExp`, `dnsbl`, `webRisk`, `database`, `snmp`, `counter` (`list_monitor_types` is the live list). |
| `url` | string | for most types | The address to monitor. A Counter monitor may carry `settings.probeUrl` instead. |
| `name` | string | no (default: the url) | Display name. |
| `interval` | integer, **seconds** | no (default: the account's default cadence) | Check interval. Must be one of the account's allowed intervals and not below the type's floor. |
| `tags` | string, comma-separated | no | Tags for the new monitor. |
| `pools` | string, comma-separated | yes for HTTP, API, ping, port, content check, page speed and transaction | Location pool ids, e.g. `westeurope,easteurope,northamerica`, or `allworld` for everywhere. Unlike the app, the API applies no account default. Refused for types that run on HostTracker's own network (SSL expiry, domain expiry, DNSBL, Web Risk, database, SNMP, counter). |
| `enabled` | boolean | no (default `true`) | `false` creates it paused. |
| `settingsJson` | string, JSON object | per type | Type-specific settings with their v2 names, e.g. `{"keywords":"Checkout","keywordMode":"PresentAny"}`. Fields per type: [Monitor fields by type](/reference/operator-monitor-fields/). |
| `dryRun` | boolean | no | `true` validates and previews, creating nothing. |

**`update_monitor`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `id` | string | yes | The monitor id. |
| `name`, `url` | string | no | New name or address. |
| `interval` | integer, **seconds** | no | New interval. |
| `tags` | string, comma-separated | no | **Replaces** the whole tag set. Do not combine with `addTags`/`removeTags`. |
| `addTags` | string, comma-separated | no | Tags to add, keeping the others. |
| `removeTags` | string, comma-separated | no | Tags to remove, keeping the others. |
| `pools` | string, comma-separated | no | **Replaces** the location pools. |
| `settingsJson` | string, JSON object | no | Settings to change; members you leave out keep their stored values. |

The type cannot be changed, and `enabled` is changed with `pause_monitor` / `resume_monitor`.

**`delete_monitor`**, **`pause_monitor`**, **`resume_monitor`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `id` | string | yes | The monitor id. |
| `confirmed` | boolean | `delete_monitor` only (default `false`) | Without it the tool only shows the monitor it would delete. |

**`copy_monitor`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `id` | string | yes | The monitor to copy from. |
| `urls` | string, comma-separated | yes | The new addresses, one copy each. |
| `includeAlerts` | boolean | no (default `true`) | Copy the alert subscriptions. |
| `includeReports` | boolean | no (default `true`) | Copy the report subscriptions. |
| `includeMaintenance` | boolean | no (default `true`) | Copy the maintenance-window coverage. |
| `name` | string | no (default: the address) | Name for the copies. |

**`bulk_create_monitors`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `itemsJson` | string, JSON array | yes | Monitor definitions, each shaped like a `POST /monitor` body: `[{"type":"http","url":"a.com","locations":{"pools":["allworld"]}}]`. |
| `defaultsJson` | string, JSON object | no | Members applied to every item, e.g. `{"interval":300,"tags":["prod"]}`. |
| `submit` | boolean | no (default `false`) | `false` returns the validation report only; `true` creates the batch as a job. |
| `idempotencyKey` | string | no (generated) | Reuse it on a retry so the batch is not created twice. |

**`bulk_update_monitors`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `filterJson` | string, JSON object | yes | The selection: any of `monitorIds`, `tags`, `types`, `states`, `enabled`, `q`, e.g. `{"tags":["prod"]}`. |
| `patchJson` | string, JSON object | one of `patchJson` / `operation` | The change applied to each selected monitor, e.g. `{"interval":300}` or `{"addTags":["eu"]}`. |
| `operation` | string | one of `patchJson` / `operation` | `resetStats` clears the statistics instead of patching. |
| `submit` | boolean | no (default `false`) | `false` returns the count and a sample only; `true` applies it as a job. |
| `idempotencyKey` | string | no (generated) | Reuse it on a retry. |

**`bulk_delete_monitors`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `filterJson` | string, JSON object | yes | The selection, as for `bulk_update_monitors`. |
| `expectedCount` | integer | to delete | The `matched` count the validation step reported. |
| `confirmed` | boolean | to delete (default `false`) | Needed together with `expectedCount`. Without both, the tool only validates. |
| `idempotencyKey` | string | no (generated) | Reuse it on a retry. |

`list_monitor_types` takes no arguments.

### Results and incidents

| Tool | What it does | Scope |
|---|---|---|
| `get_uptime_summary` | Uptime, SLA and response-time figures over a window (`GET /monitor/result/summary`). | `monitor:read` |
| `list_monitor_results` | One monitor's raw check results, newest first. | `monitor:read` |
| `list_incidents` | Down-episodes across the account or chosen monitors, newest first. | `monitor:read` |
| `get_incident` | One incident with the transitions that opened and closed it. | `monitor:read` |
| `comment_incident` | Sets a note on an incident; it replaces any previous one. | `monitor:write` |

**`get_uptime_summary`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `monitor` | string, comma-separated | yes | Monitor ids, up to 500. For a whole-account figure pass `monitor=""` together with `groupBy="account"`. |
| `from`, `to` | integer (Unix seconds) | no (default: the last 30 days) | The window. |
| `bucket` | string | no (default `none`) | `none` (one row per monitor), `hour`, `day`, `week` or `month`. |
| `groupBy` | string | no (default `monitor`) | `monitor` or `account` (one row for the whole selection; window up to 30 days). |
| `sla` | number | no | A target percentage, e.g. `99.9`; adds `slaMet` and `errorBudgetSecRemaining`. |
| `metrics` | string, comma-separated | no | `responseTime`, `dns`, `connect`, `tls`, `ttfb`, `transfer`. |
| `limit`, `cursor` | integer, string | no | Paging. |

This tool has no `expand` and no `sort` argument. Rows come back ordered by monitor id, not by uptime - sort them
yourself (see [Sort a summary](/reference/operator-reports-and-summaries/#sort-a-summary-worst-first)). For
incident counts per row, call the API through `api_request` with `expand=incidentCounts`.

**`list_monitor_results`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `monitorId` | string | yes | The monitor id. |
| `from`, `to` | integer (Unix seconds) | no | The window (at most 30 days). |
| `state` | string, comma-separated | no | `up`, `down`. |
| `location` | string, comma-separated | no | Location (agent) ids from `list_locations(agents=true)`. The tool text says names; the API takes ids. |
| `expand` | string, comma-separated | no | `metrics`, `recheck`. |
| `limit`, `cursor` | integer, string | no | Paging. |

**`list_incidents`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `monitor` | string, comma-separated | no (default: the whole account) | Monitor ids. |
| `state` | string, comma-separated | no | `open`, `resolved`. |
| `severity` | string, comma-separated | no | `minor`, `major`, `critical`. |
| `from`, `to` | integer (Unix seconds) | no | Incidents that overlap this window (not clipped to it). |
| `limit`, `cursor` | integer, string | no | Paging. |

**`get_incident`**: `id` (string, required - the incident id), `expand` (string, comma-separated, optional:
`monitor`, `recheck`).
**`comment_incident`**: `id` (string, required), `comment` (string, required - replaces any previous comment).

### Maintenance windows

| Tool | What it does | Scope |
|---|---|---|
| `list_maintenance` | Lists windows. | `monitor:read` |
| `create_maintenance` | Schedules a window over an explicit list of monitors (`POST /maintenance`). | `monitor:write` |
| `update_maintenance` | Changes only what you pass. | `monitor:write` |
| `delete_maintenance` | Cancels a window. Cancelling an active window makes its monitors alert again at once. | `monitor:write` |

**`list_maintenance`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `state` | string, comma-separated | no | `scheduled`, `active`, `finished`. |
| `monitor` | string, comma-separated | no | Monitor ids. |
| `from`, `to` | integer (Unix seconds) | no | The window. |
| `limit`, `cursor` | integer, string | no | Paging. |

**`create_maintenance`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `name` | string | yes | Window name. |
| `from` | integer (Unix seconds) | yes | Start instant. |
| `monitorIds` | string, comma-separated | yes | The monitors it covers - a fixed list; there is no tag selector. |
| `durationSec` | integer | one of `durationSec` / `to` | Length in seconds. |
| `to` | integer (Unix seconds) | one of `durationSec` / `to` | End instant. Sending both is refused. |
| `timezone` | string | no (default `UTC`) | IANA zone, e.g. `Europe/Berlin`; matters for weekly windows. |
| `suppressAlerts` | boolean | no | Suppress alerts. See the note below. |
| `suppressStats` | boolean | no | Exclude the window from uptime statistics. |
| `weekDays` | string, comma-separated | no | Makes it a weekly window, e.g. `Saturday,Sunday`. |

When you send **neither** `suppressAlerts` nor `suppressStats`, the window suppresses alerts only. When you send
**either one**, the other counts as `false` - so for "silence alerts and exclude from statistics" send both
`suppressAlerts=true` and `suppressStats=true`. Sending both as `false` is refused.

**`update_maintenance`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `id` | string | yes | The window id. |
| `name`, `timezone` | string | no | New name or zone. |
| `from`, `to` | integer (Unix seconds) | no | New start or end. |
| `durationSec` | integer | no | New length. |
| `monitorIds` | string, comma-separated | no | **Replaces** the coverage. |
| `enabled` | boolean | no | Switch the window off or on without deleting it. |

This tool cannot change the suppression flags, the weekly days or `showOnStatusPage`; use `api_request` with
`PATCH /maintenance/{id}` for those.

**`delete_maintenance`**: `id` (string, required), `confirmed` (boolean, default `false` - without it the tool only
shows the window).

### Contacts and contact groups

| Tool | What it does | Scope |
|---|---|---|
| `list_contacts` | Lists contacts. An unconfirmed contact receives nothing until confirmed. | `contact:read` |
| `get_contact` | Reads one contact. | `contact:read` |
| `create_contact` | Meant to create an `email`, `sms`, `voiceCall` or `webPush` contact - see the caution below. | `contact:write` |
| `update_contact` | Partially updates a contact. Changing the address re-triggers confirmation. | `contact:write` |
| `delete_contact` | Deletes a contact and every subscription it had. Preview first, then `confirmed=true`. | `contact:write` |
| `send_contact_confirmation` | Sends (or resends) the confirmation code. The code is never returned to the assistant - ask the person for it. | `contact:write` |
| `confirm_contact` | Confirms a contact with the code it received. | `contact:write` |
| `test_contact` | Sends a real test alert to a confirmed contact and reports how delivery ended. SMS and voice may cost balance. | `contact:write` |
| `list_contact_groups` | Lists contact groups. | `contact:read` |
| `create_contact_group` | Creates a named set of contacts, each with the events it should receive. | `contact:write` |
| `update_contact_group` | Renames a group and/or replaces its membership. | `contact:write` |
| `delete_contact_group` | Deletes a group; the contacts themselves stay. | `contact:write` |

:::caution[create_contact cannot create a contact today]
The API requires an `Idempotency-Key` header to create an `email`, `sms` or `voiceCall` contact, and
`create_contact` never sends one, so those calls answer `400 idempotency_key_required`. A `webPush` contact needs a
browser push subscription the tool cannot pass. Create email, SMS and voice contacts with the generic door instead:

```
api_request(method="POST", path="/contact",
            bodyJson="{\"type\":\"email\",\"address\":\"ops@example.com\",\"name\":\"Ops inbox\"}",
            idempotencyKey="create-ops-email-1")
```

Messenger contacts (Telegram, Viber, Discord and the like) and web push are registered by the person in the app.
:::

**`list_contacts`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `q` | string | no | Free-text search over name and address. |
| `type` | string, comma-separated | no | Contact types, e.g. `email,sms`. |
| `confirmed` | boolean | no | `true` only confirmed, `false` only unconfirmed. |
| `id` | string, comma-separated | no | Contact ids. |
| `limit`, `cursor` | integer, string | no | Paging. |

**`get_contact`**: `id` (string, required), `expand` (string, comma-separated, optional: `subscription`,
`template`, `group`).

**`create_contact`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `type` | string | yes | `email`, `sms`, `voiceCall` or `webPush`. |
| `address` | string | yes | An email address, or a phone number in international format. |
| `name` | string | no | Display name. |
| `language` | string | no | Message language code, e.g. `en`. |
| `alertDelay` | integer, minutes | no | Delay before an alert is sent to this contact (one of the published delay steps). |

**`update_contact`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `id` | string | yes | The contact id. |
| `name`, `address`, `language` | string | no | New values. A new address must be confirmed again. |
| `alertDelay` | integer, minutes | no | New alert delay. |
| `groupedAlerts` | boolean | no | Group several alerts into one message. |

**`delete_contact`**: `id` (string, required), `confirmed` (boolean, default `false`).
**`send_contact_confirmation`**: `id` (string, required).
**`confirm_contact`**: `id` (string, required), `code` (string, required - the 5-digit code the person received).
**`test_contact`**: `id` (string, required), `alertType` (string, optional: `up`, `down` or `repeatedlyDown`).
**`list_contact_groups`**: `limit`, `cursor`.

**`create_contact_group`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `name` | string | yes | Group name. |
| `itemsJson` | string, JSON array | yes | The members: `[{"contact":"CONTACT_ID","events":["down","up"]}]`. Events: `up`, `down`, `repeatedlyDown`, `daily`, `weekly`, `monthly`, `quarterly`, `yearly`. |

Example: `create_contact_group(name="On-call", itemsJson="[{\"contact\":\"ID1\",\"events\":[\"down\",\"up\"]},{\"contact\":\"ID2\",\"events\":[\"down\",\"up\"]}]")`.
A group does not subscribe anyone by itself - see [Contact groups](/alerts/contact-groups/).

**`update_contact_group`**: `id` (string, required), `name` (string, optional), `itemsJson` (string, JSON array,
optional - **replaces** the whole membership). **`delete_contact_group`**: `id` (string, required), `confirmed`
(boolean, default `false`).

### Subscriptions

| Tool | What it does | Scope |
|---|---|---|
| `list_subscriptions` | Who is notified about what. | `subs:read` |
| `subscribe_contact` | Subscribes one contact to one monitor (`PUT /monitor/{id}/alert/{contactId}` and/or `.../report/{contactId}`). | `monitor:write` |
| `unsubscribe_contact` | Removes one contact's subscription to one monitor. | `monitor:write` |

**`list_subscriptions`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `kind` | string | no (default `alert`) | `alert` or `report`. |
| `monitorId`, `contactId` | string, comma-separated | no | Filter by monitor ids and/or contact ids. |
| `monitorQuery`, `contactQuery` | string | no | Free-text search over monitors or contacts. |
| `limit`, `cursor` | integer, string | no | Paging. |

**`subscribe_contact`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `monitorId` | string | yes | The monitor id. |
| `contactId` | string | yes | The contact id. |
| `alertTypes` | string, comma-separated | one of `alertTypes` / `frequencies` | `up`, `down`, `repeatedlyDown`. **Replaces** this pair's alert set. |
| `frequencies` | string, comma-separated | one of `alertTypes` / `frequencies` | `daily`, `weekly`, `monthly`, `quarterly`, `yearly` (email contacts). **Replaces** this pair's report set. |

**`unsubscribe_contact`**: `monitorId`, `contactId` (strings, required), `kind` (string, optional: `alert`,
`report` or `both`, default `both`).

For many monitors x contacts at once there is no curated tool: use `api_request` with `POST /alert/bulk` or
`POST /report/bulk` (both need `confirmed=true`, see [The confirmation rule](#the-api_request-confirmation-rule)),
or repeat `subscribe_contact` per pair.

### Webhooks

| Tool | What it does | Scope |
|---|---|---|
| `list_webhooks` | Lists webhooks with their enabled state and recent failure count. | `webhook:read` |
| `create_webhook` | Registers an https webhook for chosen events. The signing secret is returned once. | `webhook:write` |
| `update_webhook` | Changes url, events, monitor scope, name or enabled state. Re-enabling clears the failure counter. | `webhook:write` |
| `delete_webhook` | Unregisters a webhook; pending deliveries are dropped. Preview first, then `confirmed=true`. | `webhook:write` |
| `test_webhook` | Sends a synthetic delivery and reports the endpoint's answer. | `webhook:write` |
| `list_webhook_deliveries` | Recent deliveries of one webhook with outcome and attempts. | `webhook:read` |
| `redeliver_webhook` | Resends a recorded delivery with the same delivery id. | `webhook:write` |

**`create_webhook`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `url` | string | yes | The public https endpoint. |
| `events` | string, comma-separated | yes | Event names, e.g. `monitor.down,monitor.up`. The list is in [Webhook events](/reference/webhook-events/). |
| `name` | string | no | Display name. |
| `monitorIds` | string, comma-separated | no | Deliver only for these monitors. |
| `tags` | string, comma-separated | no | Deliver only for monitors carrying these tags. |

Send **at most one** of `monitorIds` and `tags`; with neither, the webhook covers the whole account. Sending both is
refused by the API (a scope names exactly one form). There is no `scope` argument - the tool builds the REST
`scope` object from these two. Example: `create_webhook(url="https://hooks.example.com/ht",
events="monitor.down,monitor.up", tags="prod", name="Prod down/up")`.

**`update_webhook`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `id` | string | yes | The webhook id. |
| `url`, `name` | string | no | New endpoint or name. |
| `events` | string, comma-separated | no | **Replaces** the event set. |
| `enabled` | boolean | no | Enable or disable deliveries. |
| `monitorIds` | string, comma-separated | no | **Replaces** the scope with these monitors. To scope by tags or back to the whole account, use `api_request` with `PATCH /webhook/{id}` and a `scope` object. |

**`delete_webhook`**: `id`, `confirmed` (default `false`). **`test_webhook`**: `id`, `eventName` (optional, e.g.
`monitor.down`). **`redeliver_webhook`**: `id`, `deliveryId` (the `d_...` id from `list_webhook_deliveries`).
**`list_webhooks`**: `limit`, `cursor`.

**`list_webhook_deliveries`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `id` | string | yes | The webhook id. |
| `outcome` | string, comma-separated | no | `pending`, `delivered`, `failed`, `dropped`. |
| `eventName` | string, comma-separated | no | Event names. |
| `from`, `to` | integer (Unix seconds) | no | The window. |
| `limit`, `cursor` | integer, string | no | Paging. |

### Status pages

| Tool | What it does | Scope |
|---|---|---|
| `list_status_pages` | Lists your status pages. | `statuspage:read` |
| `get_status_page` | Reads one page with its settings and components. | `statuspage:read` |
| `create_status_page` | Creates a page. It is public at its slug immediately. | `statuspage:write` |
| `update_status_page` | Changes the title and/or settings. | `statuspage:write` |
| `delete_status_page` | Deletes a page with its components, incidents and subscribers. Preview first, then `confirmed=true`. | `statuspage:write` |
| `create_status_page_incident` | Publishes an incident or a scheduled maintenance on a page and notifies its subscribers. | `statuspage:write` |
| `add_status_page_incident_update` | Appends an update to an incident's timeline and moves its state. Notifies subscribers. | `statuspage:write` |

**`create_status_page`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `slug` | string | yes | The page's permanent address; must be unique. |
| `title` | string | yes | Title shown to visitors. |
| `componentsJson` | string, JSON array | no | `[{"monitorId":"ID","name":"API","group":"Core"}]`. |
| `settingsJson` | string, JSON object | no | Page settings with their v2 names, e.g. `{"slaTarget":99.9,"showGroups":true,"features":["barCharts","uptimePercent"]}` - see [Create a status page](/status-pages/create/). |

**`update_status_page`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `id` | string | yes | The page id. |
| `title` | string | no | New title. |
| `settingsJson` | string, JSON object | no | Settings to change. They are **merged** field by field: members you leave out keep their values, and only `features` replaces its whole set. (The tool's own text says it replaces the settings object; it does not.) |

Components are replaced with `api_request` and `PUT /statuspage/{id}/component`.

**`create_status_page_incident`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `id` | string | yes | The status page id. |
| `title` | string | yes | Headline. |
| `message` | string | yes | The first timeline message. |
| `state` | string | no (default `investigating`) | `investigating`, `identified`, `monitoring` or `resolved`. |
| `kind` | string | no (default `incident`) | `incident` or `maintenance`. |
| `impact` | string | no | `minor` or `major`. |
| `componentIds` | string, comma-separated | no | Component ids from `get_status_page` - `"C1,C2"`, not a JSON array. |
| `scheduledStart`, `scheduledEnd` | integer (Unix seconds) | for `kind="maintenance"` | The planned window. |
| `idempotencyKey` | string | no (generated) | Reuse it on a retry so the post is not published twice. |

**`add_status_page_incident_update`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `id` | string | yes | The status page id. |
| `incidentId` | string | yes | The incident id. |
| `message` | string | yes | The update text. |
| `state` | string | yes | `investigating`, `identified`, `monitoring` or `resolved`. |
| `idempotencyKey` | string | no (generated) | Reuse it on a retry. |

**`get_status_page`**: `id`. **`delete_status_page`**: `id`, `confirmed` (default `false`).
**`list_status_pages`**: `limit`, `cursor`.

### Reports, jobs, account and locations

| Tool | What it does | Scope |
|---|---|---|
| `generate_report` | Requests an uptime report over monitors and a time range; answers with a job id. | `monitor:write` (the tool's text says `monitor:read`; the API requires write) |
| `list_report_types` | Lists report types, formats, sections and schedules. | none |
| `get_job` | One asynchronous job: state, progress and per-item results. | the scope of the job's operation |
| `wait_for_job` | Polls a job for up to about 30 s; call again if it is still running. | the scope of the job's operation |
| `cancel_job` | Cancels a queued or running job. Finished items are not rolled back. | the scope of the job's operation |
| `resume_job` | Continues a job whose state is `interrupted`. | the scope of the job's operation |
| `get_account` | Identity, package, usage, limits and status flags. Read-only. | `account:read` (a token; OAuth connections never get it) |
| `get_account_quota` | API quota headroom and the scopes the current token carries - the first call to make after a 403. | `account:read` |
| `get_account_usage` | How many monitors, contacts, reports and maintenance windows the account uses out of its plan. | `account:read` |
| `list_locations` | Location pools (and with `agents=true`, individual locations). | none |

**`generate_report`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `monitorIds` | string, comma-separated | yes | Up to 500 monitors. |
| `from`, `to` | integer (Unix seconds) | no (default: the last 30 days) | The range. |
| `format` | string | no (default `pdf`) | `pdf`, `csv`, `xml` or `html`. |
| `sections` | string, comma-separated | no (default `stats`) | `state`, `stats`, `outages`, `incidents`, `log`. |
| `timezone` | string | no | IANA zone the report is rendered in. |
| `language` | string | no | Report language code, e.g. `en`. |
| `idempotencyKey` | string | no (generated) | Reuse it on a retry. |

**`get_job`**: `id` (string, required), `limit` (1-50, default 20 per-item results), `cursor`.
**`wait_for_job`**, **`cancel_job`**, **`resume_job`**: `id` (string, required).
`list_report_types`, `get_account`, `get_account_quota` and `get_account_usage` take no arguments.

**`list_locations`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `agents` | boolean | no (default `false`) | `true` lists individual locations instead of pools. |
| `country` | string, comma-separated | no | ISO country codes (with `agents=true`). |
| `pool` | string, comma-separated | no | Pool ids (with `agents=true`). |
| `limit`, `cursor` | integer, string | no (default 50) | Paging. |

### Location pool ids

`pools` arguments take pool ids from `list_locations` (REST `GET /agent/pool`). The ones most requests need:

| Pool id | Where |
|---|---|
| `allworld` | Every location |
| `westeurope` | Western Europe |
| `easteurope` | Eastern Europe |
| `northamerica` | North America |
| `southamerica` | South America |
| `asia` | Asia |
| `australia` | Australia |
| `africa` | Africa |

There is no single `europe` pool: for "Europe" send `westeurope,easteurope`. The app's default for a new monitor,
when the account has no defaults of its own, is `westeurope,easteurope,northamerica`. The pool list is data, not a
fixed enum - an account may also see private pools of its own - so confirm an id with `list_locations` when in
doubt; an unknown id is refused with `422 unknown_pool` and the list of valid ones. A selection with too few
locations answers `422 insufficient_agents`. See [Monitoring locations](/reference/locations/).

### Anything else

| Tool | What it does | Scope |
|---|---|---|
| `describe_api` | Searches the real v2 operations (paths, parameters, body members) so a call can be built from the contract. | none |
| `api_request` | Calls any v2 operation no curated tool covers. The method and path must match a real operation. | the operation's own scope |

**`describe_api`**: `search` (string, optional - a path or operation-id fragment such as `/alert`, `statuspage`,
`createWebhook`; omit it to list every path).

**`api_request`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `method` | string | yes | `GET`, `POST`, `PATCH`, `PUT` or `DELETE`. |
| `path` | string | yes | The v2 path with ids filled in, e.g. `/monitor/ID/incident`. No host, no version prefix. |
| `query` | string | no | A query string (`limit=10&state=down`) or a flat JSON object of parameters. |
| `bodyJson` | string, JSON | no | The request body, for `POST`/`PATCH`/`PUT`. |
| `idempotencyKey` | string | no | Sent as the `Idempotency-Key` header. Not generated for you - pass one wherever the operation requires it (below). |
| `confirmed` | boolean | no (default `false`) | Must be `true` for the calls the confirmation rule covers. |

#### The api_request confirmation rule

`api_request` refuses a call **before sending anything** unless `confirmed=true` when:

- the method is `DELETE` (any path), or
- the method is `POST`, `PATCH` or `PUT` and the path contains `/bulk` and does **not** end in `/validate`.

No v2 path ends in `/validate` - the dry runs are spelled `...-validate` - so today the second rule covers every
bulk door **and its dry run**:

| Needs `confirmed=true` through `api_request` | What it is |
|---|---|
| `POST /alert/bulk`, `POST /report/bulk` | Many alert or report subscriptions in one transaction (answers directly, no job, no dry run) |
| `POST /monitor/bulk`, `POST /monitor/bulk-update`, `POST /monitor/bulk-delete` | Bulk monitor jobs |
| `POST /contact/bulk`, `POST /contact/bulk-delete` | Bulk contact jobs |
| `POST /monitor/bulk-validate`, `/monitor/bulk-update-validate`, `/monitor/bulk-delete-validate`, `/contact/bulk-validate`, `/contact/bulk-delete-validate` | The read-only dry runs (they change nothing, but the rule still asks for `confirmed=true`) |
| every `DELETE` | Deletes, unsubscribes, cancelling a maintenance window |

A `?dryRun=true` in `query` does not exempt a call - only the method and path are checked. Every other call
(`GET`, and writes such as `POST /contact`, `PUT /monitor/{id}/alert/{contactId}`, `PATCH /statuspage/{id}`) runs
without `confirmed`. The refusal says the call was not executed; show the user what it will do, then repeat it with
`confirmed=true`. For monitor bulk work prefer the curated `bulk_*_monitors` tools: they run the dry run for you
without any confirmation flag.

Operations that need `idempotencyKey` through `api_request` (the API answers `400 idempotency_key_required`
otherwise): `POST /monitor/bulk`, `/monitor/bulk-update`, `/monitor/bulk-delete`, `/contact/bulk`,
`/contact/bulk-delete`, `POST /monitor/report`, `POST /monitor/{id}/reset-stats`,
`POST /statuspage/{id}/incident`, `POST /statuspage/{id}/incident/{incidentId}/timeline`, `POST /contact` for an
`email`, `sms` or `voiceCall` contact, and `POST /monitor` when the body carries inline `contacts`. Sending a key on
any other write is harmless.

Writes under `/account` are always refused, whatever the token allows.

## How the tools behave

- **Deletes preview first.** Every single-resource delete tool deletes nothing on its first call - it returns the
  live resource so the assistant can confirm with you. Only a repeat call with `confirmed=true` deletes, and the API
  then returns a receipt of what was removed. `api_request` does not preview: without `confirmed=true` it refuses a
  `DELETE` or bulk call outright and sends nothing.
- **Bulk writes validate first.** The bulk tools return the validation report; the write needs `submit=true`, and a
  bulk delete also needs `confirmed=true` plus the validated `expectedCount`. If the selection changed in between,
  the API refuses with `409 selection_mismatch` and deletes nothing.
- **Async work returns a job.** Bulk operations, multi-address copies and report generation answer with a job id;
  poll it with `wait_for_job`.
- **Publishing is real.** Status page incidents and updates go to the public page and to its subscribers at once.
  Contact tests and confirmations send real messages.
- **Errors come back as tool results.** A missing scope, a quota, a validation refusal or a blacklisted URL
  returns an actionable message naming the problem code. `Retry-After` is passed through as a value; the server
  never retries or waits on its own.
- **Untrusted content is fenced.** Text a monitored target or a third-party endpoint controls (check errors, webhook
  response excerpts) is wrapped and length-capped so it cannot pass instructions to the assistant.

## What the server never does

These refusals are built into the server and apply whatever your token allows:

- Any write under `/account` (profile, email, country, default pools). Account reads stay open.
- Payments, packages and plan changes, passwords, sign-in and account recovery - none of these are on the API.
- Minting or revoking API tokens. A token cannot mint another token.

## Troubleshooting

- **Every call is refused for permissions.** The refusal names the required and granted scopes. On OAuth, remove and
  re-add the connector and approve the wider set. On a token, run `get_account_quota` to see its scopes, then mint a
  new token - scopes cannot be added to an existing one.
- **The client connects but lists no tools.** The client probably dropped the `Authorization` header; test the same
  token with `curl` and a `tools/list` call.
- **The consent page says the app is not registered.** The client's registration expired (registrations with no
  approved connection are cleaned up after about 30 days). Remove and re-add the connector.
- **Rate-limit or quota messages.** The endpoint limits requests per client IP, and your plan's API quota applies to
  the token. The message says which one bound and, when known, when it resets.
- **`mcp-remote` starts and exits.** Usually Node.js older than 18, a header argument split on its space (pass it as
  `Authorization:${HT_AUTH}` with the value in the environment), or a stale session under `~/.mcp-auth`, which you
  can delete.
- **A corporate proxy breaks the stream.** Streamable HTTP holds a long-lived response; allow
  `mcp.host-tracker.com` through without buffering.
- **A token leaked.** Tokens cannot be revoked before they expire. Remove it from every client, mint a replacement
  with a short expiration, and contact [ht2support@host-tracker.com](mailto:ht2support@host-tracker.com) (never
  include the token itself).

## Related

- [API authentication, tokens and scopes](/integrations/api-authentication/)
- [REST API v2](/integrations/rest-api/)
- [Rate limits and quotas](/integrations/rate-limits/)
- [Webhooks](/integrations/webhooks/)
