Uptimeify Docs
MCP Server

MCP Server

Use Uptimeify's free check tools from any AI agent over the Model Context Protocol (MCP).

Uptimeify exposes a stateless Model Context Protocol server so AI agents can run our free checks directly.

For an overview of what the server can do and which clients it works with, see the MCP server page on uptimeify.io. This page is the technical reference.

Endpoint

POST https://uptimeify.io/mcp: Streamable HTTP transport, stateless. The anonymous check tools need no authentication; the authenticated tools require a Bearer API token (see below).

Discover the server programmatically via the MCP Server Card:

GET https://uptimeify.io/.well-known/mcp/server-card.json

Connect an MCP client

Point any MCP client that speaks the Streamable HTTP transport at the endpoint above.

  • Anonymous tools work with just the URL: no token needed.
  • Authenticated tools require your Uptimeify API token as an Authorization: Bearer <token> header. Create one under Settings → API tokens: leave the customer field empty for an organization-wide token (sees all your monitors), or pick a customer to scope the token to that customer only. The token is shown once on creation: copy it right away.

Clients that support remote HTTP MCP servers with custom headers can point at the URL directly:

{
  "mcpServers": {
    "uptimeify": {
      "url": "https://uptimeify.io/mcp",
      "headers": { "Authorization": "Bearer wsm_your_token_here" }
    }
  }
}

For clients that only speak stdio, bridge to the HTTP endpoint with mcp-remote:

{
  "mcpServers": {
    "uptimeify": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://uptimeify.io/mcp", "--header", "Authorization: Bearer wsm_your_token_here"]
    }
  }
}

Omit the Authorization header entirely if you only need the anonymous check tools.

Rate limits

The anonymous tools run real network probes on your behalf, so they are capped per client IP. You do not need an account to hit these limits, and you do not need one to stay under them:

ScopeLimit
POST /mcp overall120 requests per minute per IP
Each anonymous check tool15-30 calls per minute per IP, depending on how expensive the probe is (whois and domain_expiry are the strictest at 15)
Each authenticated read tool60 calls per minute per IP
Each authenticated write tool10 calls per minute per organization, not per IP -- writing is rarer and costlier than reading, and a model retrying a write in a loop should hit this quickly on purpose

The two layers apply together: 120 calls a minute spread across different tools is fine, 120 calls a minute of whois is not. Exceeding a limit returns HTTP 429 with a retryAfter value in seconds, telling you when the current window ends. Windows are fixed, not sliding, so a client that waits out retryAfter gets a full fresh allowance.

The limits are enforced per IP, not per token: an API token does not raise the ceiling, and running several agents behind one NAT address makes them share it. If you have a use case that genuinely needs more, get in touch rather than working around it.

Available tools

All tools are anonymous and read-only. They mirror our public web tools:

  • check_ssl: TLS/SSL certificate inspection (host, optional port)
  • check_dns, dns_propagation, mx_lookup: DNS resolution and propagation (domain)
  • spf_check, dkim_check (domain, selector), dmarc_check: mail authentication (domain)
  • dnsbl_check (ip), whois (domain), domain_expiry (domain)
  • http_headers (url), hsts_check (domain), redirect_check (url)
  • port_check (host, port), ping_test (host, optional port)
  • website_status (url), response_time (url)
  • ip_geolocation (query), asn_lookup (query), reverse_dns (ip)

Authenticated tools (read)

These tools read your own monitoring and account data. Send your Uptimeify API token as an Authorization: Bearer <token> header on the MCP request. A customer-scoped token sees only its own customer's monitors; an organization token sees all customers in the organization. All are read-only.

  • list_monitors (optional kind): list your monitors across all eight families (website, dns, icmp, tcp, ftp, smtp, ssh, imap_pop). Omit kind for all of them; every item carries its own kind. Each item is limited to a fixed field list: identity and target (id, publicId, name, url or hostname and port, monitoringType, status, managementType), the check configuration without any credentials (interval, timeout, HTTP method, expected status codes, which checks are enabled, notice and error thresholds), state (isActive, isDown, isRecovering, hasCritical, hasWarning, inMaintenance, upSinceAt, lastCheckedAt), uptime and response-time figures, the customer's name and public id, tags (id, public id, name, colour) and timestamps. Never returned: the heartbeat token, any stored credential or encrypted setting (authorization header, basic-auth user and password, custom headers, request body, mTLS certificate and key, Playwright environment), the Playwright script, the monitor's alert phone number and e-mail, custom fields, the raw protocol configuration of the non-HTTP families, and the embedded customer record.
  • monitor_status (monitor_id, optional kind): a single monitor with its current up/down status. kind defaults to website for backward compatibility. Same field list as list_monitors; the detail endpoint's extra secrets (heartbeat token, Playwright script and environment, credential presence flags) are withheld in the same way.
  • list_incidents (optional limit): recent incidents across your monitors. The embedded customer carries id, public id and company name; the customer's contact e-mail is removed.
  • check_history (monitor_id, optional kind, from, to): recent checks, optionally within an ISO date range. kind defaults to website.
  • uptime_summary (monitor_id): uptime and average response time for day / month / year. Website monitors only -- the other seven families do not have this endpoint.
  • list_alert_channels (optional website_id): the notification channels that can raise an alert for you -- name, type (email, SMS, webhook, or one of the chat / on-call integrations), whether the channel is active, and its escalation priority and delay. Without website_id you get the organization-level channels; pass a website id from list_monitors to get the channels that apply to that monitor, including its website-level overrides. A channel's own configuration is never returned: no recipient addresses, phone numbers, webhook targets or credentials.
  • alert_history (monitor_id, optional kind, from, to, status, page, limit): which alerts went out for one monitor, when, over which channel (name and type), and whether delivery succeeded or failed with which error. Together with list_alert_channels this answers "why did nobody call me". kind defaults to website, status is sent or failed, and the result is paged: 25 entries per page by default, 100 at most. The channel's stored configuration is not returned here either. errorMessage is a different matter: it is free text from the delivery attempt itself and can quote what the receiving side said, so a failed send may name the SMTP host and port, a recipient address the mail server rejected, or the hostname of a webhook target.
  • list_status_pages: your status pages -- name, slug, description, the customer each belongs to, whether it is published, its visibility (public, customer_members_only or password_protected) and whether a shared page password is set, the monitors the page deliberately hides, and the custom domain it is served on. Takes no arguments; the list follows from the token. Subscriber lists are never returned, and neither is the page's visual design or the DNS verification token of a custom domain.
  • list_maintenance_windows (optional active_only): your scheduled maintenance windows -- name, start and end time, the IANA timezone the recurrence is interpreted in, whether and how it recurs, whether it is active, and which monitors it covers. A monitor inside a window does not alert, which is the usual answer to "why was there no incident". This endpoint has no date-range filter: the answer always covers every window you can see, past and future alike, so bound it by time yourself instead of assuming the list was filtered.
  • get_organization: your own organization -- its numeric id, public id, name, account status (active or suspended) and, when a deactivated account is scheduled for deletion, the date that happens. Takes no arguments: the organization follows from the token. Those five fields are the entire answer. Billing address and cycle, payment method, discount codes, quota and price limits, the MFA policy and the organization's default notification settings are not returned, and the tool never reaches the SMTP, audit-log or data-export endpoints under /api/organization/.
  • list_users: the members of your organization -- name, role (admin, editor or readonly), whether the member is still active, and when they joined, alongside limits.currentSeats, the number of members contained in the answer. Takes no arguments. The answer carries no member identifier at all, and that is deliberate: a member can be named but not referenced, so any follow-up that points at a person has to go through the name -- and two members who share a name cannot be told apart. No e-mail addresses, no avatars, no customer assignments either. Note the scope of the list: it is narrowed to the caller's customer scope, the same way GET /api/users is. A customer-scoped token sees only the members assigned to its own customer, plus itself -- and limits.currentSeats counts exactly those, so it is not the head count of the organization.
  • list_customers: the customers of your organization -- numeric id, public id, name, account status and the date the customer was created, plus how many monitors each one has (total, managed, self-service and DNSBL entries). Takes no arguments, and a customer-bound token sees only its own customer. Not returned: contact and notification e-mail, postal address, VAT id, phone number, cancellation dates, free-form custom fields, and the tariff name -- the tariff is not part of this answer at all.
  • billing_summary: the SMS usage of the current month -- the organization-wide total and the per-customer breakdown. Takes no arguments, and SMS usage is the only billing figure it reports: never a tariff or plan, an amount of money, a currency, an invoice or billing date, a billing address, a billing e-mail or a payment method. Its answer depends on a second area: with mcp:billing:read alone, each customer in the breakdown is its public id and its usage figure and nothing else; the customer's name and status appear only when mcp:organization:read was granted as well. Names belong to the organization area, so a connection that declined it does not get the customer list back through the usage figures. A customer-bound token sees only its own customer.

Six tools carry a positive list and emit the fields named above and nothing else: the four account tools, plus list_monitors and monitor_status, so a column added to the endpoint later does not travel out with them. Five remove a handful of named fields from whatever the endpoint returned -- list_alert_channels, alert_history, list_status_pages, list_maintenance_windows and list_incidents -- and two, check_history and uptime_summary, hand the endpoint's answer on untouched because it contains nothing but check results. A field you do not see there is withheld on purpose, not by oversight.

Paging differs per tool, and only alert_history lets you ask for a further page. list_monitors returns at most one page per monitor family (50 entries each) per call; total is the real count of matching items across the families queried, not the number of items returned, so if the two differ the list was truncated. check_history returns the endpoint's first page, 10 checks within its default 30-day window, and reports total, page and pageCount alongside. alert_history pages on its own arguments: 25 entries per page by default, 100 at most, steered by page and limit. list_alert_channels, list_status_pages, list_maintenance_windows, list_users, list_customers and billing_summary are not paged -- each returns everything the token can see, so a short list there is a short list, not a truncated one. get_organization answers with a single record and has nothing to page.

When list_monitors runs without kind, it queries all eight families in parallel. If one of them does not answer, the result still comes back, and both the text and structuredContent.partialFailures name the sources that are missing. Treat such a list as incomplete rather than as "you have none of those".

Over OAuth these tools are gated per area: the two alert tools need mcp:alerts:read and the two status-page tools need mcp:statuspages:read, both in the Monitoring and operations group of the consent screen; get_organization, list_users and list_customers need mcp:organization:read and billing_summary needs mcp:billing:read, both in the Account and billing group (see below). A tool whose area was not granted does not appear in tools/list at all, so a client never learns to call one it cannot use. A wsm_ API token is not divided this way and reaches all 13.

If you call one of these without a token, the tool returns an error asking for the Authorization: Bearer header. Create a token under Settings → API tokens. A request that carries no Authorization header at all does not see them in the first place: tools/list then returns only the 20 anonymous tools. That is the same rule as above, applied to the caller who granted nothing -- a client is never shown a tool it cannot call.

Authenticated tools (write)

43 tools change your monitoring data instead of just reading it. They call the same /api endpoints the REST API and the dashboard use -- no separate write API exists, and no new endpoint was added to build them. Two things distinguish them from every read tool above, and both are permanent, not launch caveats:

  • OAuth only. A wsm_ API token never sees a write tool, no matter how it is scoped -- the reasoning is an owner decision, see Connected apps. Only an OAuth connection holding the matching write scope can call one.
  • Deployment-gated. Write tools exist in the tool catalogue, but a deployment registers them only when it has explicitly turned them on. Where they are not enabled, none of the 43 appear in tools/list and calling one by name fails as if it did not exist, regardless of which scopes your connection holds. Check with whoever runs your Uptimeify instance before assuming they are live.

A connection needs the matching write scope to see a tool at all: mcp:monitors:write unlocks the first six below, mcp:incidents:write the next four, mcp:maintenance:write the next three, mcp:statuspages:write the next eight, mcp:alerts:write the next three, mcp:organization:write the last nineteen. Checking a write scope on the consent screen also checks its read twin of the same name (for example mcp:monitors:write checks mcp:monitors:read too) -- most of these tools compare the record's current state before writing so a repeated call does not repeat the write, and that comparison needs read access to the same data. mcp:maintenance:write is the one exception: it has no read twin of its own, because list_maintenance_windows already reads under mcp:statuspages:read and a second scope for the same data would be a checkbox that decides nothing. Checking it therefore does not also grant mcp:statuspages:read -- a connection needs that scope separately before update_maintenance_window's state comparison (see below) can run. None of the three alert-channel tools makes a state comparison either -- there is no GET for a single channel through this connection, so a repeated call always writes again. Under mcp:organization:write exactly two of the nineteen compare state, update_user and update_customer, and both only when the connection also holds mcp:organization:read. With this area every one of the six write scopes now backs at least one tool; none is left empty.

Monitor tools (mcp:monitors:write), all working across the eight monitor families (website, dns, icmp, tcp, ftp, smtp, ssh, imap_pop) unless noted:

  • pause_monitor (kind, monitor_id): stop checking a monitor. While paused it raises no alerts, and the paused period shows as a gap in its uptime figures rather than downtime. Calling it on an already-paused monitor changes nothing and says so instead of writing again.
  • resume_monitor (kind, monitor_id): start checking a paused monitor again. Left untouched if the monitor is in maintenance rather than paused -- ending a maintenance window is a separate, deliberate action this tool never takes. Subject to the same monitor-quota check as creating a monitor, so it can fail outright if the quota is exhausted.
  • rename_monitor (kind, monitor_id, name, 1-255 characters): change the display name. Cosmetic only -- what is checked, how often, and its alert routing all stay the same.
  • set_check_interval (kind, monitor_id, check_interval_minutes, 1-60): change how often a monitor is checked. The account's package sets a minimum interval per monitor; a value below it fails the call outright instead of being rounded up, so a lower interval is not always available.
  • create_website_monitor (customer_id, name, url, optional check_interval_minutes, optional monitoring_type): create a website monitor for a specific customer. customer_id is required and should never be guessed -- ask which customer the monitor belongs to if it is not already known. Subject to the account's monitor quota, so it fails outright rather than queuing if the quota is exhausted. Carries no credentials or scripts: no basic-auth password, mTLS key, custom authorization header, or Playwright script/environment, and monitoring_type excludes playwright for the same reason -- set those up in the dashboard.
  • delete_monitor (kind, monitor_id, confirm: true): delete a monitor permanently, taking its check history, incidents, alert channels, tags and maintenance windows with it. This cannot be undone. Refused with readonlyMayNotDelete for a caller whose role is readonly, regardless of who owns the monitor. confirm must be true, and should be set only after a human asked for this specific deletion -- it is not a box a model should tick reflexively.

Incident tools (mcp:incidents:write), covering both manual (status-page) and automatic (monitor-raised) incidents unless noted, and all four requiring the admin or editor role -- a readonly or supporter caller is refused, generically, on every one:

  • acknowledge_incident (incident_id): mark an incident as being worked on. No-op if it is already acknowledged. Acknowledging a resolved incident is a genuine reopen -- Uptimeify treats both open and acknowledged as "still live" -- so it schedules a status-page "reopened" event and re-notifies subscribers who were told it was fixed. Use it on a resolved incident only when the incident has genuinely recurred.
  • resolve_incident (incident_id): resolve an incident. On a genuine open/acknowledged -> resolved transition this schedules a status-page "resolved" event and mails subscribers who were told about the outage. Calling it again on an already-resolved incident is a no-op and never sends a second all-clear.
  • post_incident_update (incident_id, status: investigating, identified, monitoring or resolved, body, up to 5000 characters): post a new public update to an incident's timeline; it appears on the status page immediately. Not idempotent -- every call publishes a distinct, new update, so a call whose response never arrived should not be repeated, that republishes the same news a second time. Posting status: 'resolved' also resolves the parent incident (equivalent to resolve_incident) and mails the all-clear; posting any other status on a currently-resolved incident reopens it (equivalent to acknowledge_incident's reopen) and re-notifies subscribers.
  • delete_incident (incident_id, confirm: true): delete a manual incident (a status-page announcement) permanently, taking every status-page event recorded about it with it. This cannot be undone. Refused with notManualIncident for an automatic, monitor-raised incident -- those are worker-owned and can never be deleted through this tool. confirm must be true, set only after a human asked for this specific deletion.

Maintenance window tools (mcp:maintenance:write):

  • create_maintenance_window (name, start_time, end_time, one of customer_id or tag_ids, optional description, optional timezone): schedule a maintenance window -- while it runs, the monitors it covers raise no alerts and the period shows as planned maintenance rather than downtime. Requires exactly one of customer_id (every monitor of that customer) or tag_ids (every monitor carrying those tags); a call with neither fails outright. Not idempotent -- a second identical call creates a second window and publishes a second status-page announcement to subscribers. An organization-wide window (tag_ids without customer_id) requires an unrestricted scope and admin or editor role; everyone else gets a window scoped to their own customer.
  • update_maintenance_window (maintenance_window_id, optional name, description, start_time, end_time, is_active): change a scheduled window's name, description, time range or active state. Deactivating a window (is_active: false) stops it suppressing alerts without deleting it. When the connection can also read maintenance windows (holds mcp:statuspages:read), this compares against the current window first and reports no change instead of writing when every field already matches; without that read scope the comparison is skipped and the call writes regardless.
  • delete_maintenance_window (maintenance_window_id, confirm: true): delete a maintenance window permanently. If the window is currently running, its monitors stop being suppressed immediately and any failure they are in becomes a real alert. confirm must be true, set only after a human asked for this specific deletion.

Status page tools (mcp:statuspages:write):

  • create_status_page (customer_id, name, optional slug, description, visibility, is_published, subscriptions_enabled, subscription_delay_minutes): create a new status page for a specific customer. customer_id is required and should never be guessed -- ask which customer the page belongs to if it is not already known. Not idempotent -- a second call, even with the same name, creates a SECOND page: the handler appends -2 (then -3, and so on) to the slug rather than reusing the existing one, so do not repeat a call whose response did not arrive. Carries no password argument: a password-protected page needs a password this tool would reject a call without, so set one up in the dashboard instead. Presentation (layout, colors, which sections show) is not settable here -- call update_status_page_design after creating the page.
  • update_status_page (status_page_id, optional customer_id, name, slug, description, visibility, is_published, subscriptions_enabled, subscription_delay_minutes): change a status page's name, slug, description, visibility, publish state, customer, or subscriber settings. There is no read endpoint for a single status page through this connection, so this tool cannot compare against the current page first: every call writes, even one that would set the same values again. Carries no password argument, for the same reason create_status_page does not, and no show_recent_incidents/show_recent_maintenance -- the handler mirrors both into the page's design configuration as a side effect, which would silently collide with update_status_page_design, so that presentation belongs to the design tool instead.
  • update_status_page_design (status_page_id, optional layout, color scheme, accent color, header style, font, card radius, page width, custom title/subtitle, and which sections show, each with its own limits and lookback windows for incidents and maintenance): change a status page's visual design. Reachable by a caller whose role is readonly, unlike the other three status-page tools, which need admin or editor. When the connection can also read status pages (holds mcp:statuspages:read), compares against the current design first and reports "no change" without writing if every field this comparison can see already holds the requested value; without that read scope, or when the request sets a field the comparison cannot see (the history/incident/maintenance display settings), the comparison is skipped and the call writes regardless.
  • delete_status_page (status_page_id, confirm: true): delete a status page permanently, taking its subscriber list with it -- every subscriber row cascades on delete. This cannot be undone. confirm must be true, set only after a human asked for this specific deletion.
  • add_status_page_domain (status_page_id, hostname): bind a custom domain to a status page. Creates the row as pending and returns the TXT record name and value the human must publish in DNS. Publishing the record does not finish the job: call verify_status_page_domain afterwards to prove control of the zone, then activate_status_page_domain to start serving the hostname. Not idempotent -- a status page can hold only one domain, so a second call for a page that already has one fails rather than replacing it.
  • verify_status_page_domain (domain_id): look up the DNS TXT record for a pending status page domain and, on success, promote it from pending to verified. Fails while the record is not yet visible in DNS -- this is the normal state right after add_status_page_domain, often for as long as DNS propagation takes, so do not call this again in a tight loop. Takes domain_id as a plain number, unlike the id-or-public-id form the monitor and status page tools use. Requires the admin role -- an editor is refused here even though editors can call the other two domain tools.
  • activate_status_page_domain (domain_id): promote a verified status page domain to active, the moment the hostname actually starts serving the page. Performs no DNS lookup of its own -- that is verify_status_page_domain's job -- but re-checks that this row still holds the claim on the hostname. Requires the admin role, the same restriction as verify_status_page_domain.
  • delete_status_page_domain (status_page_id, domain_id, confirm: true): delete a status page's custom domain permanently. The status page itself is not deleted -- it becomes reachable only at its default URL again. confirm must be true, set only after a human asked for this specific deletion.

Alert channel tools (mcp:alerts:write):

  • create_notification_channel (channel_type: email or sms, name, email_address or phone_number, optional customer_id, priority, delay_seconds, is_active): create a notification channel. Only email and sms are offered here -- every other channel type (Slack, Telegram, a generic webhook, and 27 more) carries credentials in its configuration and is set up in the dashboard instead. Not idempotent -- a second identical call creates a SECOND channel; there is no uniqueness on name and no state comparison, so do not repeat a call whose response did not arrive. Without customer_id this creates an organization-wide channel and needs the admin role; with customer_id it creates a customer-bound channel, which a readonly caller may also create for their own customer.
  • update_notification_channel (channel_id, optional name, priority, delay_seconds, is_active): change a channel's display name, escalation priority, delay, or whether it is active. Does not touch a channel's configuration -- not its target, not its type, no credentials; change the target in the dashboard. There is no GET for a single channel through this connection, so this tool cannot compare against the current channel first: every call writes, even one that would set the same values again.
  • delete_notification_channel (channel_id, confirm: true): delete a notification channel permanently -- this cannot be undone. To pause a channel without losing it, call update_notification_channel with is_active: false instead. confirm must be true, set only after a human asked for this specific deletion.

Organization tools (mcp:organization:write), nineteen of them, the only area whose tools leave the monitoring data behind and touch the account itself. Seven are destructive. Two, update_user and update_customer, compare state before writing when the connection also holds mcp:organization:read; every other one in this area writes on every call. Tags first:

  • create_tag (name up to 24 characters, color from a ten-key palette, optional customer_id): create a tag for grouping monitors. Not idempotent -- tag names are not unique and there is no GET for a single tag here, so a second identical call creates a SECOND tag. Without customer_id the tag is organization-wide; a caller restricted to several customers who names none of them fails with tagCustomerRequired. There is no role gate beyond globalsupporter here: even a readonly caller may create a tag, which is the reverse of create_custom_field under the very same scope.
  • update_tag (tag_id, optional name, color): rename a tag or change its colour. Never moves it to another customer. No state comparison is possible, so every call writes. Who may change a tag is decided by the tag, not the caller's role: a global admin, its creator, an unrestricted caller, or one whose scope contains the tag's customer.
  • delete_tag (tag_id, confirm: true): delete a tag everywhere, permanently. It falls off every monitor, and it is stripped from every maintenance window that used it -- a window whose scope was only this tag is then left with no scope at all and may suppress nothing. The response reports how many windows were touched in affectedWindows, counted before the delete; check those windows afterwards. confirm must be true, set only after a human asked for this specific deletion.
  • add_monitor_tag (kind, monitor_id, tag_id): attach an existing tag to one monitor. The endpoint is itself idempotent, but this tool reports changed: true either way, so read the answer as "the tag is now on this monitor", not as "something was written". At most FIVE tags per monitor; a sixth is refused with tagLimitReached (422, carrying max), the only 422 in this catalogue. Both ids are numeric.
  • remove_monitor_tag (kind, monitor_id, tag_id, confirm: true): take a tag off ONE monitor. The tag itself stays and keeps every other monitor -- deleting it everywhere is delete_tag, a much larger action. An assignment that never existed also answers with success. confirm is required because every delete here requires it, not because the consequence is grave: a single add_monitor_tag undoes it.

Custom fields, three tools, all three requiring the admin role -- stricter than create_tag under the same scope, so being allowed there says nothing about being allowed here:

  • create_custom_field (name, field_type, optional field_key, options, placeholder, help_text, is_required, show_in_table, sort_order): define a field on EVERY customer record of this organization at once. That is a schema change, not data on one customer, and with is_required it changes what other people must fill in. Not idempotent: field_key is derived from name when omitted, so a repeated name collides with customFieldAlreadyExists (409), not a 400. No tool here lists custom fields, so keep the id from the response.
  • update_custom_field (custom_field_id, optional name, field_type, options, placeholder, help_text, is_required, show_in_table, sort_order, is_active): change a field definition. Never its field_key, which is how every value already captured is addressed. No comparison is possible, so every call writes. is_active: false has exactly the same effect as delete_custom_field and deserves the same human confirmation even though this tool carries none. Changing field_type does not convert values already stored.
  • delete_custom_field (custom_field_id, confirm: true): despite the name this deletes nothing. The endpoint marks the definition inactive; the definition and every value already captured under it on every customer stay in the database, untouched. What changes is visibility: the field leaves the customer forms, the customer table, and the one listing its id could still be read from. Keep the id first, because update_custom_field with is_active: true is the way back. A retired field keeps its field_key occupied, so create_custom_field with the same name still fails with customFieldAlreadyExists (409). Treat it as destructive nonetheless: for the people in the dashboard, the field and its data are gone.

Scheduled reports, three tools, all three requiring the admin role. No tool here reads a report back, so nothing can compare against what already exists:

  • create_report (name, recipient_user_ids, optional frequency, weekday, day_of_month, scope_mode, scope_customer_ids, scope_tag_ids, monitor_types, format, enabled): create a scheduled report that mails a summary to named organization members, repeatedly, starting on its next due date with no further step from anyone. Not idempotent -- report names are not unique, so a second call creates a SECOND report that then mails the same people on the same schedule, findable only by a human. The schedule defaults to monthly, so day_of_month (1 to 28) is required unless frequency is daily; weekly requires weekday. Recipients must be members of this organization; free-form addresses are deliberately not offered here. Keep the id from the response.
  • update_report (report_id, plus any of the create fields): change a report's name, schedule, scope, monitor families, format, recipients or whether it sends. report_id is the report's public id, a UUID. Every call writes; there is nothing to compare against. recipient_user_ids REPLACES the recipient list rather than adding to it. The cross-field checks run against the MERGED report, not against what you sent. enabled: false stops the sending without losing the report and is the reversible alternative to deleting it.
  • delete_report (report_id, confirm: true): delete a scheduled report permanently. Its run history goes with it, together with the stored path of each run's PDF; the PDF file itself is not removed from object storage, so it stays in the bucket with nothing left pointing at it. Already-delivered mails and the separate per-customer reports are untouched. If the intent is only to stop the sending, use update_report with enabled: false instead. confirm must be true, set only after a human asked for this specific deletion.

One tool changes a member. There is deliberately no create_user and no delete_user in this connection -- inviting someone and removing someone both happen in the dashboard, so do not try either name:

  • update_user (user_id, optional first_name, last_name, is_active): change one member's profile. It changes no role and no customer assignment, so a call here can neither grant nor take away a permission. Compares before it writes when the connection also holds mcp:organization:read. Pass first_name and last_name together whenever you change either: the endpoint rebuilds the displayed full name out of exactly the two values in the request. is_active: false locks the member out within seconds but deletes nothing, and is_active: true undoes it. Requires the admin role and an unrestricted token; a customer-scoped token is refused with customerScopedTokenForbidden (403). list_users deliberately returns no member ids, so user_id has to come from a human or the dashboard.

Customers and the two billable entries that hang off them, seven tools:

  • create_customer (name, one of package_id or package_type, optional contact fields): create a new customer under this organization. It creates NO monitor -- the customer starts empty. Two organization-wide preconditions are checked first: quotaRequired (403) when the organization has chosen no monitor quota, and billingSetupRequired (403) when it has no valid direct-debit mandate. A package selects quota and feature flags and carries no price. Not idempotent -- customer names are not unique. Requires admin or editor and an unrestricted token. Keep the publicId from the response: the other six all address the customer by it.
  • update_customer (customer_id, optional name, contact fields, status, package_id, alert-routing overrides, monthly-report flag): change an existing customer. Compares before it writes when the connection also holds mcp:organization:read, which matters here because the endpoint writes one audit entry per changed field. status: inactive is the heaviest thing this tool does: every monitor of all eight families, every monitored domain and watched IP is switched off, running maintenance windows stop, and published status pages leave the internet. Setting it back to active restores the monitors, domains and IPs but not the status pages or the windows.
  • delete_customer (customer_id, confirm: true): delete a customer and the widest cascade in this catalogue -- 21 tables lose their rows with it, including its monitors, domains, IPs, incidents, status pages, maintenance windows, API tokens, notification channels and the stored PDFs of reports already produced for it. If the intent is only to stop the monitoring, use update_customer with status: inactive instead. Requires the admin role, strictly more than update_customer needs. The report PDFs are removed from object storage BEFORE the row, and a failed removal fails the whole call rather than leaving personal PDFs behind with nothing pointing at them.
  • add_customer_domain (customer_id, domain_name, optional status, notes): watch a domain's registration expiry. This creates a billable monitor and consumes both organization and package quota. An organization at its limit fails with monitorQuotaReached (403), unless automatic upgrading is on, in which case this very call moves it to a higher and more expensive tier; one already on the top tier fails with monitorQuotaNoHigherTier. Only registries that answer RDAP are supported. Not idempotent and there is no uniqueness check, so a repeat creates a second billable monitor. No tool here lists these entries, so keep the id and publicId.
  • delete_customer_domain (customer_domain_id, confirm: true): delete one watched domain permanently. Its expiry status row and its tag assignments go with it; the tags themselves stay. The only route back is add_customer_domain with the same name, which creates a NEW entry with a new id. A readonly caller is refused with readonlyMayNotDelete (403) whatever else it may edit. confirm must be true, set only after a human asked for this specific removal.
  • add_customer_ip (customer_id, ip_address, optional status, notes): watch an IP address on the DNSBL blocklists. This creates a billable monitor against the organization's active-IP limit, and that limit answers with a bare 403 carrying no data.code at all, so the message has to be read rather than the code matched. The address alone decides IPv4 or IPv6; there is deliberately no argument for it. Not idempotent and there is no uniqueness check. Keep the id and publicId from the response.
  • delete_customer_ip (customer_ip_id, confirm: true): delete one watched address permanently. Its DNSBL status row, its whole listing-check history and any maintenance window bound to it go with it, and its tag assignments are removed while the tags stay. The only route back is add_customer_ip with the same address, which creates a NEW entry with an empty history. A readonly caller is refused with readonlyMayNotDelete (403). confirm must be true.

What this area deliberately does not reach: creating or deleting a member, bulk customer maintenance, sending a report by hand, the organization's own master data, SMTP settings and API tokens. All of those stay in the dashboard.

A write tool returns { changed: true, result } for a real write, or { changed: false, reason } when a state comparison found nothing to do. pause_monitor, resume_monitor, rename_monitor, set_check_interval, acknowledge_incident, resolve_incident, update_maintenance_window, update_status_page_design, update_user and update_customer make that comparison (update_maintenance_window only when the connection also holds mcp:statuspages:read; update_status_page_design only when it holds mcp:statuspages:read and the request sets no field the comparison cannot see; update_user and update_customer only when it holds mcp:organization:read); create_website_monitor, delete_monitor, post_incident_update, delete_incident, create_maintenance_window, delete_maintenance_window, create_status_page, update_status_page, delete_status_page, add_status_page_domain, verify_status_page_domain, activate_status_page_domain, delete_status_page_domain, create_notification_channel, update_notification_channel, delete_notification_channel, create_tag, update_tag, delete_tag, add_monitor_tag, remove_monitor_tag, create_custom_field, update_custom_field, delete_custom_field, create_report, update_report, delete_report, create_customer, delete_customer, add_customer_domain, delete_customer_domain, add_customer_ip and delete_customer_ip do not -- a delete already answers idempotently at the endpoint itself, the channel, tag, custom-field and report tools have no read endpoint to compare against, and the rest are deliberately never a no-op. Errors come back the same way as for the read tools above: isError: true with a plain message, no structuredContent.

Structured output

Every tool declares an outputSchema, and every successful call returns structuredContent next to the text block. You can type-check a response against the published schema instead of parsing our JSON by hand. The text block is unchanged and still carries the full payload, so a client that ignores structured output sees exactly what it saw before.

Two properties of these schemas are deliberate:

  • No field is required. The schema types the fields it knows about; it never asserts one is present. An endpoint that omits or nulls a value does not turn a check into a failed tool call.
  • Extra properties are allowed. The schemas are open, so a field added later rides along in structuredContent instead of failing validation in a strict client.

structuredContent must be a JSON object, and four of these tools answer with a bare array: list_incidents has its array wrapped under an incidents key, list_alert_channels under channels, list_maintenance_windows under maintenanceWindows, and list_customers under customers. The text block keeps the array as-is in all four cases.

Errors carry no structuredContent. They come back as isError: true with a plain message, as before.

Tool names use only letters, digits and underscores. That is not cosmetic: the major function-calling APIs validate tool names against ^[a-zA-Z0-9_-]{1,64}$ (OpenAI) and ^[a-zA-Z0-9_-]{1,128}$ (Anthropic), so a dotted name would be rejected by any client that passes our tools through to a model.

Connect via OAuth (Claude Desktop / Claude.ai)

Uptimeify's /mcp endpoint also supports standard MCP OAuth (OAuth 2.1 + PKCE + Dynamic Client Registration): this is how Claude's remote-MCP connector authenticates without you creating an API token by hand.

  1. In Claude, add https://uptimeify.io/mcp as a remote MCP server and choose Connect.
  2. Claude registers itself automatically (Dynamic Client Registration) and opens a browser window asking you to sign in to Uptimeify and approve access.
  3. The consent screen names the application and the account it will be linked to, and offers each area the application requested as a separate checkbox, grouped in up to three sections: Monitoring and operations (monitors, incidents, check history, alerts, status pages -- read), Account and billing (organization, billing -- read) and Write access (monitors, incidents, maintenance windows, status pages, alert channels, organization -- see Authenticated tools (write) above; check history and billing are the two read areas with no write counterpart). Claude's remote-MCP connector requests the seven read areas, so that is the widest version of the read half of the screen you will see; an application that asks for read access without naming a specific area gets only the three operational areas that existed before this expansion -- monitors, incidents, check history -- and only those three appear, pre-checked. Whichever areas are offered, the read areas start pre-checked (account areas unchecked, because that is where personal and billing data lives) and every write area starts unchecked: nothing here is granted by default, ever. Checking a write box also checks its read twin of the same name, because the tools need to read a record before deciding whether writing it would change anything -- unchecking the write box afterwards leaves the read grant in place. Maintenance windows are the one write area with no read twin to check: list_maintenance_windows lives under the status-pages read area instead, so check that separately if you want update_maintenance_window to compare state before writing. Clear a box and that area stays out of the connection. Outside of a checked write area the connection stays exactly as read-only as before: it inherits your own account's visibility, a customer-restricted user's Claude sees only that customer, while an org admin's Claude sees the whole organization -- and even inside a checked write area, whether a tool actually does anything depends on whether this deployment has write tools switched on at all (see above).
  4. Once approved, Claude sees only the tools whose area you granted, and none of the others. All seven read areas carry tools, the same 13 read-only tools available to wsm_ API tokens: list_monitors and monitor_status (monitors), list_incidents (incidents), check_history and uptime_summary (check history), list_alert_channels and alert_history (alerts), list_status_pages and list_maintenance_windows (status pages), get_organization, list_users and list_customers (organization), billing_summary (billing). Billing is the one area that also reads better with a second one: granting organization alongside it puts customer names on the usage breakdown, and leaving it out keeps the breakdown to public ids. Checking the six write areas, where the deployment has them switched on, adds the 43 tools listed above -- wsm_ API tokens never get these, however they are scoped. The anonymous check tools are unaffected by any of this.

There is no way past the screen. An authorization request that omits prompt, or sends any other value, is answered with the consent screen regardless. prompt=none is the one request we do not rewrite: it asks for no interface, so it gets the OpenID Connect error redirect instead, carrying login_required without a session and consent_required with one. The scope names themselves are documented under Connected apps.

OAuth access tokens are short-lived (1 hour) and refresh silently while Claude keeps using the connection; the underlying grant expires automatically after 7 days of inactivity. You can review and revoke connected applications at any time under Settings → Connected apps (see Connected apps): revoking is immediate and reconnecting requires going through OAuth consent again.

OAuth and static wsm_ API tokens both work on /mcp at the same time: connecting via OAuth does not require removing an existing API-token integration.

Example (tools/list)

curl -X POST https://uptimeify.io/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Without an Authorization header this lists the 20 anonymous tools. Add an API token or an OAuth connection to see the account-bound ones your grant covers.

Example (authenticated tool call)

List your own monitors: add the Authorization header:

curl -X POST https://uptimeify.io/mcp \
  -H 'Authorization: Bearer wsm_your_token_here' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_monitors","arguments":{}}}'

On this page