# MCP Server Security: Read-only, Token Scopes and Human Approval

> Why an AI agent can read your Uptimeify data but has no structural path to write anything.

Source: https://uptimeify.io/blog/mcp-server-security-read-only

An AI agent reading your monitoring data cannot change anything in your Uptimeify account. That is not a promise, it is a property of the token: every write attempt from an agent token ends in `403` with the error code `agentTokenReadOnly`. So does every GET request outside the allowlist of its scope. This post shows where that boundary runs, who opens it and how you close it again in one call.

- Agent tokens are read-only. Writes, and reads outside the scope allowlist, end in `403 agentTokenReadOnly`.
- An anonymous registration reaches only `tools.public`, meaning the public check tools. It sees no account data.
- `api.read` exists only after a claim flow that a signed-in human confirms with the exact bound email address. On the anonymous path only that human ever sees the confirmation code, never the agent.
- The code expires after 600 seconds, the access token after 3,600 seconds. Revocation through `/oauth2/revoke` is idempotent and always returns `200`.

## The real question: not "may the agent in", but "what can it do in there"

Most security conversations about MCP servers circle around access. That is the weaker question. Access is binary, blast radius is not. Four things decide the actual risk: which verbs the token knows, which surface it sees, how long it lives and who approved it.

The Uptimeify MCP server is stateless and runs at `POST https://uptimeify.io/mcp`. It exposes two groups of tools. 20 check tools work with no account and no token at all: SSL check, DNS lookup, DNS propagation, MX, SPF, DKIM, DMARC, DNSBL, WHOIS, domain expiry, HTTP headers, HSTS, redirects, port check, ping, website status, response time, IP geolocation, ASN and reverse DNS. Thirteen more tools read your own monitoring and account data and do require a token: `list_monitors`, `monitor_status`, `list_incidents`, `check_history`, `uptime_summary`, `list_alert_channels`, `alert_history`, `list_status_pages`, `list_maintenance_windows`, `get_organization`, `list_users`, `list_customers` and `billing_summary`. The last four are the narrowest of the set: they emit a fixed list of fields and nothing else, so no contact e-mail, no address, no VAT id, no phone number, no tariff name and no payment method leaves with them.

None of these 33 tools writes. An agent checking the TLS chain of `deinkunde.com` for you, or verifying that the SPF record still resolves, needs no access to your account whatsoever. That is the first and most effective cut: most everyday agent work happens outside your data.

## Read-only as architecture, not as a setting

For agent tokens, read-only is not a checkbox on a form. It is what the token is. Every write call, and every `GET` or `HEAD` outside the allowlist of its scope, is rejected with 403 and `data.code = agentTokenReadOnly`. There is no path through which an agent token creates a monitor, edits a customer or closes an incident. Those operations stay with humans holding a login or a classic API token.

There are exactly two scopes:

| Scope | How it comes about | What it reaches |
| --- | --- | --- |
| `tools.public` | anonymous registration, active immediately, no approval needed | only the public check tools under `GET /api/tools/*` |
| `api.read` | only after a confirmed claim by a signed-in human | additionally `GET /api/websites*`, `GET /api/incidents*` and `GET /api/health`, scoped to the approving user's organization or a single customer inside it |

The registration and token endpoints are served only on the canonical host `uptimeify.io`. On a white-label domain of your agency they answer with `404`. Your client status pages stay outside the auth flow, even when they run under your own domain.

Read-only here does not mean "the agent chooses to behave". It means the write path does not exist for this token.

## Who grants access: the human, not the agent

Before an agent may read account data at all, a signed-in human has to confirm it. The claim flow knows two routes, and the difference between them is the interesting part.

With a `service_auth` registration the ceremony starts immediately. The registration sits at `pending` and mints no token until a signed-in user authorizes it. The agent receives the `user_code` and the `verification_uri` and displays both to its human, the way a device flow on a television does.

With an anonymous registration it is stricter. That registration works right away with `tools.public` and can optionally be upgraded to `api.read` later. On this path the agent never learns the `user_code`. It gets a verification URL and nothing else. The code is visible only to the signed-in user with the exact bound email address, on the claim page. An agent cannot talk itself into its own upgrade, not even when it fully controls the registration.

Two more properties make the flow auditable:

- A registration can be claimed exactly once. A second attempt returns `409 claimed_or_in_flight`.
- The resulting scope hangs off the approving human. A token never reaches further than the user who authorized it, either across that user's organization or a single customer inside it.

That answers the most common question from client conversations before it is asked: no, an agent does not see other tenants' data, and no, it cannot grant itself the access.

Which tools the MCP server offers, which of them run without an account, and how to add it to your client are all on the overview page.

## Token lifetime and revocation

Short lifetimes are the second line of defence after scope. If a token lives for one hour, a leak is an incident with an expiry date instead of a standing condition.

- The **access token** is opaque, starts with `wsma_` and expires after 3,600 seconds. After that the agent mints a fresh one from its identity assertion.
- The **identity assertion** is an EdDSA JWT valid for roughly 24 hours. It is the durable credential, not the access token.
- The **confirmation code** in the claim flow and its claim token both expire after 600 seconds. Too slow means starting the ceremony again.
- **Revocation** through `POST /oauth2/revoke` is idempotent and always returns `200`, including for an unknown or long revoked token. The result is unambiguous instead of sending you into a debugging session.

On top of that come limits that slow a runaway agent down early: 120 requests per minute per IP on the MCP endpoint, 15 to 30 per minute per anonymous tool, 60 per minute per authenticated tool. Anything above returns `429`. Registration is capped at 30 requests per minute, the token endpoint at 60.

## What you have to treat like a password

In exactly one place the secret does not live in a header, it lives in the path. When you create an alert source in Incident Management, for Zabbix, Grafana, Datadog, Sentry, Prometheus Alertmanager or a custom webhook, Uptimeify generates an ingest URL shaped like `https://<your-domain>/api/im/ingest/<token>`. The token is part of the URL. The full URL therefore has to be treated like a password: not in a public repository, not in a ticket, not in a shared screenshot.

Three details that make handling it easier:

- The URL is shown in cleartext exactly once, right after the source is created. Never again after that.
- It can be rotated at any time. The source's settings tab issues a fresh URL and retires the old one.
- An unknown or rotated-away token returns `404 not_found`. The response never reveals whether that token ever existed. That is deliberate, and it denies a scanner the feedback it would need to guess.

The same discipline applies to classic API tokens. They start with `wsm_`, are shown in full only once, and can be created with a lifetime between 1 and 365 days. Scoping one to a single customer scopes the damage with it. What a token is technically, and where you use it, is covered in the glossary entry on the API token.

## Where the data sits

Everything queried through the MCP server runs on the same infrastructure as the rest of the platform: European-only stack, Frankfurt, without US subprocessors. The MCP server is not a separate service with its own data store, it is a read surface on the existing API. It copies nothing to another place. The current list of providers in use is published on the [subprocessors](/subprocessors) page, and the technical detail on encryption, access control and operations sits on the pages for [GDPR and EU hosting](/features/trust-and-compliance/gdpr-eu-hosting) and [enterprise security](/features/trust-and-compliance/enterprise-security).

Part of the data question is how long things stay. In Incident Management, alert payloads are nulled 90 days after ingest, resolved alerts are deleted 90 days after ingest, and outbound delivery records are deleted 90 days after they were sent. The incident itself and its timeline are kept in full for 12 months, after which only a daily aggregate survives, covering incident counts plus acknowledge and resolve times, for 24 months in total. An agent with `api.read` only ever sees the normalized view, never the raw payload of an alert source. That payload is not served by this interface at all, because it can contain whatever the operator of the source put into it, including someone else's keys.

If you find a gap in how we secure this, the route for reporting it is documented: [security policy](/security/policy).

## Checklist before you approve

Before you grant an agent `api.read`, walk these seven points. Together they take under ten minutes and save you the uncomfortable version of the conversation.

1. **Does the agent need account data at all?** For checks against third-party domains the public tools are enough, with no registration.
2. **Who confirms the approval?** The claim hangs off a named, signed-in person with a bound email address. Record who that was.
3. **Scoped to what?** Organization or single customer. When in doubt, the smaller slice.
4. **Where does the agent run?** A token in a local client is a different object from one in a shared automation. The one-hour expiry helps, it does not replace storage discipline.
5. **Are ingest URLs stored properly?** They carry their token in the path. Visible in repos, tickets or screenshots means: rotate.
6. **Is the revocation route known?** `POST /oauth2/revoke`, idempotent, always `200`. Looking it up during an incident costs minutes you do not have.
7. **Can you evidence the data answer?** Location, subprocessors and retention periods are documented. Link them in the proposal instead of describing them.

Security around agent access does not come from someone promising to be careful. It comes from the risky operation having no path at all. Everything else is trust without evidence.
