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

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.
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.
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.
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/revokeis idempotent and always returns200, 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 page, and the technical detail on encryption, access control and operations sits on the pages for GDPR and EU hosting and 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.
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.
- Does the agent need account data at all? For checks against third-party domains the public tools are enough, with no registration.
- Who confirms the approval? The claim hangs off a named, signed-in person with a bound email address. Record who that was.
- Scoped to what? Organization or single customer. When in doubt, the smaller slice.
- 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.
- Are ingest URLs stored properly? They carry their token in the path. Visible in repos, tickets or screenshots means: rotate.
- Is the revocation route known?
POST /oauth2/revoke, idempotent, always200. Looking it up during an incident costs minutes you do not have. - 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.
Frequently asked questions
No. Agent tokens are read-only. Every write attempt returns status code 403 with the error code agentTokenReadOnly, and so does every GET request outside the allowlist of the token's scope. This is not a toggle in the interface that someone can flip by accident, it is a property of the token itself. Creating, editing and deleting stays with humans holding a regular login or a classic API token.
The access token expires after 3,600 seconds, so one hour. After that the agent has to mint a fresh one from its identity assertion. The assertion itself is valid for around 24 hours. The confirmation code in the approval flow expires after 600 seconds.
Through POST /oauth2/revoke. The call is idempotent and always returns status code 200, even for an unknown or already revoked token. You never get a puzzle back, you get a confirmation. On top of that every access token expires within the hour anyway, so a forgotten revocation does not leave a standing permission open.
Only as far as the approving human reaches. A claimed token with api.read is scoped to the organization of the user who confirmed the approval, or to a single customer inside it. An anonymous registration without approval reaches only the public check tools and sees no account data at all.
On the same European infrastructure as the rest of the platform: Frankfurt, European-only stack, 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. The current list of providers in use is published on the subprocessors page.
Co-Founder of Uptimeify, responsible for all of marketing. He bridges technical development and marketing strategy: from Java, PHP and Shopware plugins to steering digital growth strategies. A certified UX Manager (IHK) and digital-marketing advisor to three non-profit organizations.
More from the blog

What Is an MCP Server? Definition, Function and Practice
An MCP server is the standardised interface an AI assistant uses to call real tools instead of guessing.

Platform Update July 2026: 27 changes, two of them ask something of you
New probe locations, a full data export, TCP port monitoring and a second SMS carrier: every change from July in one post.

Detect Defacement: Monitor Website Manipulation Before Your Clients Do
Why a hijacked client site stays unnoticed for days, and how to cut your response time down to minutes.



