Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
MDX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327---category: Referencetitle: MCP Serverdescription: Connect openstatus to AI assistants via the Model Context Protocol---
The openstatus MCP server lets AI assistants (Claude.ai, Claude Desktop, Claude Code, ChatGPT, Cursor, and other [Model Context Protocol](https://modelcontextprotocol.io) clients) read and manage your status pages, status reports, and maintenance windows directly from a conversation.
## Endpoint
```https://api.openstatus.dev/mcp```
The transport is **Streamable HTTP** (stateless). The server speaks JSON-RPC 2.0 over a single endpoint that accepts `GET`, `POST`, and `DELETE`.
## Authentication
The MCP server accepts two credentials. OAuth is the default for interactive clients; the API key header stays for CI and headless agents.
For a guided walkthrough of registering, signing in, and verifying the connection, see [Connect openstatus to your coding agent](/docs/guides/how-to-connect-openstatus-to-your-agent).
### OAuth (recommended)
Point your client at `https://api.openstatus.dev/mcp` with no header. The server answers `401` with a `WWW-Authenticate` header pointing at its protected-resource metadata, and any client that implements the [MCP authorization spec](https://modelcontextprotocol.io/specification/basic/authorization) (Claude.ai, Claude Desktop, Claude Code, ChatGPT, Cursor, VS Code) takes it from there:
1. The client registers itself once (dynamic client registration) and opens the authorization URL in your browser.2. You sign in to openstatus if needed, pick the **workspace** to connect and the **access** level (read-only or read & write), and click **Approve**.3. The client exchanges the code for an access token (PKCE `S256`) and starts calling tools with `Authorization: Bearer os_oat_โฆ`.
There is nothing to copy. The token is scoped to the workspace you picked at consent, so a user in several workspaces connects each one as a separate app.
| | ||---|---|| Access token | `os_oat_` prefix, valid for 1 hour, refreshed automatically by the client || Refresh token | 90 days sliding, rotated on every refresh || Grant | one live grant per client and user; connecting again replaces it || Revoke | **Settings > Integrations > Connected apps**; the next call fails with `401` |
A connection lives in **Settings > Integrations > Connected apps** with the client name, granted access, who connected it, and when it was last used. Members revoke their own connections, owners and admins revoke any. Removing a member from the workspace revokes their connections too.
### API key (fallback)
Send an openstatus API key in the `x-openstatus-key` header. This is the same key the REST API, ConnectRPC, CLI, and Terraform provider use, and the right choice when no browser is available: CI pipelines, cron jobs, or an agent running on a server.
```httpx-openstatus-key: os_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx```
Get a key from the dashboard's **Settings > General**, in the **API Keys** card. When both credentials are present, the header wins.
### Scopes
Both credentials carry a scope that controls what tools the MCP server exposes:
- **Read-only** โ the server only registers read tools (`list_*` and `get_*`). Mutation tools (`create_*`, `update_*`, `add_*`, `resolve_*`) are not advertised in `tools/list` and cannot be called.- **Read & write** โ every tool is available.
With OAuth you choose the scope on the consent screen; a client may request read & write and you can still downgrade it to read-only. On an API key the scope is set at creation time and is **immutable**: to change it, revoke the key and issue a new one.
For AI agents that should only observe state โ health summaries, paging on-call, drafting communication โ grant **Read-only**. The server gates writes at two layers (the `tools/list` filter is the visible UX; the service-layer scope check is the boundary), so a misconfigured client can't accidentally mutate.
## Tools
The server exposes 23 tools (21 on plans without the `audit-log` feature), grouped by resource: 19 scoped to the workspace tied to your credential, plus 4 public content tools that read openstatus.dev pages on any credential and any scope (see [Content](#content)). Mutations write to the audit log with `actor_type = "mcp"` (see [Audit log](#audit-log)).
### Pages
| Tool | Type | Purpose ||------------------------|------|---------|| `list_status_pages` | read | List public status pages with their slug and id. Used to discover the `pageId` required by mutation tools. || `list_page_components` | read | List components on a status page with their id, name, type (`monitor` / `static`), and linked monitor. Used to discover the `pageComponentIds` accepted by `create_status_report`, `update_status_report`, and `create_maintenance`. |
### Status reports
| Tool | Type | Purpose ||----------------------------|----------|---------|| `list_status_reports` | read | List status reports newest-first. `filter: "active" \| "all"` (defaults to active = excludes resolved). Paginated via `page` (1-indexed) and `perPage`; response carries a `pagination` object with `page`, `perPage`, `totalSize`, and `totalPages`. || `create_status_report` | mutation | Create a new status report on a status page with an initial public update. || `add_status_report_update` | mutation | Append a public update to an existing status report and bump its status. || `update_status_report` | mutation | Edit a report's title, status, or affected components without posting a public update. || `resolve_status_report` | mutation | Mark a report resolved and post a final public update with the supplied message. |
### Maintenance
| Tool | Type | Purpose ||----------------------|----------|---------|| `list_maintenances` | read | List maintenance windows newest-first. Paginated via `page` (1-indexed) and `perPage`; response carries a `pagination` object with `page`, `perPage`, `totalSize`, and `totalPages`. || `create_maintenance` | mutation | Schedule a maintenance window (`from` / `to` are ISO 8601 strings). |
### Monitors
| Tool | Type | Purpose ||-----------------------|------|---------|| `list_monitors` | read | List monitors newest-first, including `activeIncidentCount`. Used to discover the numeric `monitorId` the other monitor tools require. || `get_monitor` | read | Full configuration for one monitor: URL, regions, periodicity, retry/timeout, notification channels, tags. Does not return latency or status. || `get_monitor_status` | read | Per-region current health (active / degraded / error). No time window. || `get_monitor_summary` | read | Aggregate success/degraded/error counts, p50โp99 latency, and `lastPingAt` over `1d` (default), `7d`, or `14d`. || `list_response_logs` | read | Recent per-region HTTP check results โ request status, status code, latency โ over `1d` (default), `7d`, or `14d`. HTTP monitors only; `limit` โค 100. || `get_response_log` | read | Full detail of one check: URL, timing breakdown (dns/connect/tls/ttfb/transfer), redacted response headers, error message, assertion results. |
### Notifications
| Tool | Type | Purpose ||----------------------|------|---------|| `list_notifications` | read | List notification channels and the monitors each one is wired to. Channel credentials are never exposed. |
### Private locations
| Tool | Type | Purpose ||--------------------------|------|---------|| `list_private_locations` | read | List the workspace's private locations with their name, status (`active` / `error`), metadata, and `lastSeenAt`. Agent tokens are never exposed. |
### Audit
Registered only when the workspace plan includes the `audit-log` feature โ otherwise these tools are absent from `tools/list`.
| Tool | Type | Purpose ||-------------------|------|---------|| `list_audit_logs` | read | List audit-log entries (mutating actions) newest-first, bounded to the last 14 days. Optional `entityType` + `entityId` filter. || `get_audit_log` | read | Full before/after snapshots and `changedFields` for a single audit-log entry. |
### Content
Public openstatus.dev content โ no workspace data, available on any credential and any scope.
| Tool | Type | Purpose ||--------------------|------|---------|| `search_docs` | read | Search the documentation, guides, or changelog (`type`, default `docs`). Returns title, snippet, url, and a `path` for `get_doc_page`. || `get_doc_page` | read | Full markdown of one docs, guide, or changelog page by `path`. || `search_content` | read | Search every public page โ product/pricing, blog, comparisons, use cases, customer stories, tooling, plus docs/guides/changelog. `type` defaults to `all`. Returns title, type, snippet, url, and a `path` for `get_content_page`. || `get_content_page` | read | Full markdown of any public page by `path` (e.g. `pricing`, `blog/โฆ`, `compare/โฆ`). |
The MCP client gates every tool call behind your approval โ the server does not gate again. Each tool also carries [MCP annotations](https://modelcontextprotocol.io/specification/server/tools#tool-annotations) (`readOnlyHint`, `destructiveHint`, `idempotentHint`) so well-behaved clients can decide whether to confirm, cache, or surface differently.
### Resources
Alongside its tools, the server exposes a small set of read-only MCP **resources** โ publicdocuments carrying no workspace data, so they are available on any credential and any scope.List them with `resources/list` and fetch one with `resources/read`:
| Resource | URI | Contents ||---|---|---|| `openapi-specification` | `https://api.openstatus.dev/openapi.json` | The full OpenAPI document, served from the binary rather than fetched. || `mcp-server-reference` | `https://www.openstatus.dev/docs/reference/mcp-server` | This page. || `site-index` | `https://www.openstatus.dev/llms.txt` | `llms.txt`: product context, pricing, and a linked index of every page on openstatus.dev. |
Remotely fetched resources are capped at a 5-second timeout and never resolve empty: on a failurethe read returns a short pointer back to the same URI rather than a blank body.
### Notifying subscribers
Every publishing tool โ `create_status_report`, `add_status_report_update`, `resolve_status_report`, `create_maintenance` โ has a **required** `notify: boolean` field, with no default. The tool's input schema rejects calls that omit it, which forces the LLM to make an explicit choice (and therefore ask the user) before firing. `update_status_report` edits metadata only and carries no `notify` field at all.
This required-field behaviour is specific to MCP. The dashboard AI assistant and the Slack agent wrap these same tools in an approval step that strips `notify` from the model-facing schema and injects it from a human toggle defaulting to `false`. MCP exposes the raw schema, so the caller must supply `notify` explicitly.
Notifications dispatch as part of the same call. There is no separate notify tool: if you create a status report or append an update with `notify: false`, that update will **never** reach subscribers โ you cannot retroactively notify the same update later. This matches the dashboard and Slack agent semantics.
The mutation and the notify dispatch are sequential, not transactional. The mutation persists first; if the notify step then throws (transient provider issue, partial outage of an integration), the response carries `notified: false` and the row stays.
`notified: true` means the dispatch call returned without throwing โ **not** that every subscriber received a message. If the workspace plan doesn't include subscriber notifications, the service is a silent no-op and the response will still report `notified: true`. Treat the field as "the dispatch ran cleanly," not as a delivery receipt.
| Tool | What `notify: true` sends ||----------------------------|---------------------------|| `create_status_report` | Notification for the initial update || `add_status_report_update` | Notification for the new update || `resolve_status_report` | Resolution notification || `create_maintenance` | Maintenance scheduled notification || `update_status_report` | n/a โ metadata-only edit, never has a notify path |
The required `notify` field, combined with the mandatory draft-and-confirm workflow in each tool's description, encodes the contract that LLMs must:
1. Draft the title/status/message/components.2. Show the draft to the user.3. Ask **explicitly** whether to notify subscribers.4. Only call the tool once both content and notify are confirmed.
The tool's response includes a `notified: boolean` field so the assistant can confirm what actually went out. If the workspace plan doesn't include subscriber notifications, `notify: true` is a no-op inside the service (no error, just nothing dispatched).
### Lookup-before-mutate
`create_status_report` and `create_maintenance` require a `pageId`, and they (plus `update_status_report`) accept an optional `pageComponentIds`. The tool descriptions instruct the model to call `list_status_pages` for the `pageId` and `list_page_components` for component ids first; **never** type a numeric id you don't know โ make the assistant resolve it. Ids that don't belong to the workspace (or, for components, to the supplied page) come back as `NOT_FOUND`.
## Configure Claude Code
```bashclaude mcp add --transport http --scope user openstatus https://api.openstatus.dev/mcp```
Run `/mcp` inside Claude Code, select **openstatus**, and choose **Authenticate**. Your browser opens the consent screen; approve, and the tools appear. Step-by-step in [Connect openstatus to Claude Code](/guides/connect-openstatus-to-claude-code).
For a headless setup, pass the API key instead: `--header "x-openstatus-key: os_โฆ"`.
## Configure Claude.ai and Claude Desktop
Claude.ai and Claude Desktop share one connector list. Open **Settings > Connectors > Add custom connector**, enter `https://api.openstatus.dev/mcp` as the URL, and click **Connect**. Approve on the consent screen and enable the connector in the chat's tool picker.
If you need an API key instead (Claude Desktop only), bridge the server through [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) in `claude_desktop_config.json`:
```json{ "mcpServers": { "openstatus": { "command": "npx", "args": [ "mcp-remote", "https://api.openstatus.dev/mcp", "--header", "x-openstatus-key: ${API_KEY}" ], "env": { "API_KEY": "os_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } }}```
The key is held in `env` and referenced via `${API_KEY}` because `mcp-remote` parses header args before shell expansion. Fully quit and relaunch Claude Desktop afterwards.
## Configure Cursor
Add the server to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
```json{ "mcpServers": { "openstatus": { "url": "https://api.openstatus.dev/mcp" } }}```
Cursor shows the server as **Needs login**; click it to run the OAuth flow. To use an API key instead, add `"headers": { "x-openstatus-key": "os_โฆ" }` to the entry.
## Configure opencode
Add the server to `opencode.json` (project) or `~/.config/opencode/opencode.json` (global), then run `opencode mcp auth openstatus` to complete the OAuth flow:
```json{ "$schema": "https://opencode.ai/config.json", "mcp": { "openstatus": { "type": "remote", "url": "https://api.openstatus.dev/mcp", "enabled": true } }}```
To use an API key instead, add `"headers": { "x-openstatus-key": "{env:OPENSTATUS_API_KEY}" }` and `"oauth": false` to the entry. Step-by-step in [Connect openstatus to opencode](/guides/connect-openstatus-to-opencode).
## Configure Codex
```bashcodex mcp add openstatus --url https://api.openstatus.dev/mcpcodex mcp login openstatus```
The first command writes the server to `~/.codex/config.toml`, which the Codex CLI, the Codex IDE extension, and the ChatGPT desktop app all share; the second opens the consent screen. For a headless setup, pass the API key instead: `--header "x-openstatus-key=os_โฆ"`, or `env_http_headers = { "x-openstatus-key" = "OPENSTATUS_API_KEY" }` in a committed `.codex/config.toml`. Step-by-step in [Connect openstatus to Codex](/guides/connect-openstatus-to-codex).
## Configure ChatGPT
In ChatGPT on the web, switch on **Developer mode** under **Settings > Security and login**, then open **Settings > Connectors > Create**, enter `https://api.openstatus.dev/mcp` as the MCP server URL with **OAuth** authentication, and complete the consent screen. Enable the connector per chat from the **+** menu under **Developer mode**. Step-by-step in [Connect openstatus to ChatGPT](/guides/connect-openstatus-to-chatgpt).
## OAuth endpoints
For client authors. openstatus is an OAuth 2.1 authorization server for the `/mcp` resource, with public clients only.
| Endpoint | Purpose ||---|---|| `GET /.well-known/oauth-authorization-server` | Authorization server metadata (RFC 8414) || `GET /.well-known/oauth-protected-resource/mcp` | Protected resource metadata for `/mcp` (RFC 9728) || `POST /oauth/register` | Dynamic client registration (RFC 7591) || `GET /oauth/authorize` | Authorization request; redirects to the consent screen || `POST /oauth/token` | `authorization_code` and `refresh_token` grants; form or JSON body || `POST /oauth/revoke` | Token revocation (RFC 7009); accepts either token of a grant |
- `response_type=code` with PKCE `S256` is required. Scopes are `read` and `write`; an authorization request without a scope asks for `write`.- Clients are public (`token_endpoint_auth_method: none`). There are no client secrets.- Dynamic registration only accepts redirect URIs on loopback hosts, `https` URLs on `openstatus.dev`, `claude.ai`, `chatgpt.com`, `cursor.com` and their subdomains, or the `cursor://`, `vscode://`, and `vscode-insiders://` schemes. Loopback redirects match on everything but the port (RFC 8252).- Any other client can skip registration and use a **Client ID Metadata Document**: pass a public `https` URL with a path as `client_id`. The document is fetched on every authorization request, must echo its own URL as `client_id`, and its `redirect_uris` are used as-is. Domain ownership replaces the allowlist.- Errors follow RFC 6749 (`{ "error", "error_description" }`), not the openstatus API envelope.
### Self-hosting
The issuer is fixed by configuration, not derived from the request host, so metadata stays stable behind proxies. Set both on the API server:
```envOAUTH_ISSUER=https://api.example.com # origin of /mcp and /oauth/*DASHBOARD_URL=https://app.example.com # serves /oauth/consent```
Outside production `OAUTH_ISSUER` defaults to `http://localhost:3000` and `DASHBOARD_URL` to `http://localhost:3001` โ the ports of `pnpm dev`. The Docker stack binds different ports; see [How to Self-Host openstatus](/docs/guides/self-hosting-openstatus) for the values that go with it.
## Errors
Errors map by severity:
- **Recoverable** (`NOT_FOUND`, `VALIDATION`, `CONFLICT`, `LIMIT_EXCEEDED`, `PRECONDITION_FAILED`) come back as a tool result with `isError: true`. The model can read the message and retry with corrected input.- **Transport-level** (`UNAUTHORIZED`, `FORBIDDEN`, `INTERNAL`, malformed input) come back as a JSON-RPC error.
Error messages are not redacted โ the consumer is an LLM that benefits from the detail.
## Limits
- `list_status_reports` and `list_maintenances` use offset pagination: `page` (1-indexed, default 1) and `perPage` (default 50, max 200). The response's `pagination.totalPages` tells the LLM whether more pages exist. `list_status_pages` is unpaginated (workspaces typically have a handful).- `list_monitors` and `list_audit_logs` page via `perPage` (max 50); `list_notifications` via `perPage` (max 200); `list_private_locations` via `perPage` (max 100); `list_response_logs` via `limit` (max 100) and `offset`.- Every tool invocation is one server call and counts against the [rate limits](#rate-limits) below.
## Audit log
Every mutation invoked through MCP appears in the workspace audit log with:
- `actor_type = "mcp"` โ slice by surface: `WHERE actor_type = 'mcp'`.- `actor_id` = the credential's stable identifier (not the workspace id). For an API key this is the key id; for an OAuth connection it is `oat_<grant id>`. Trace a specific credential with `WHERE actor_id = '<id>'`.- `actor_user_id` = the openstatus user behind the credential: who created the API key (custom keys only), or who approved the OAuth consent. Attribute mutations back to a person with a join to the `user` table.
Connecting and revoking an app are audited too, as `oauth_grant.create` and `oauth_grant.delete`.
## Rate limits
MCP requests share the per-token limits of the HTTP API. A rejected request gets a plain `429` with a `Retry-After` header rather than a JSON-RPC error; see [API rate limits](/docs/reference/api-rate-limits).