---
title: "MCP Server"
url: "https://www.openstatus.dev/docs/reference/mcp-server"
date: "2018-10-20"
author: "openstatus"
description: "Connect openstatus to AI assistants via the Model Context Protocol"
category: "Reference"
---

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.

```http
x-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_*`, and the incident tools `declare_*`, `set_*`, `draft_*`, `approve_*`) 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 40 tools (38 on plans without the `audit-log` feature), grouped by resource: 36 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, each with its `updates` timeline (newest first, with ids, capped at the 10 most recent) and `latestUpdate`. `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`. |
| `get_status_report`        | read     | Get one status report with its full `updates` timeline (newest first, with ids, uncapped) and `pageComponentIds`. Use it to reach entries older than the 10 that `list_status_reports` returns. |
| `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. |
| `update_status_report_update` | mutation | Edit the status, message, date or component impacts of an existing update. |
| `delete_status_report_update` | mutation | Remove an update. A report keeps at least one; the report's status is re-derived. |

### 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). |
| `add_maintenance_update` | mutation | Post a dated progress note on a maintenance window (`date` optional, defaults to now). |
| `update_maintenance_update` | mutation | Edit the message or date of an existing note. |
| `delete_maintenance_update` | mutation | Remove a note. The window and its announcement stay. |

### Incidents

These tools act on declared incidents, not on monitor downtime, and are internal: none of them publishes to a status page or notifies subscribers. See the [Incident management reference](/docs/reference/incident-management).

| Tool                  | Type     | Purpose |
|-----------------------|----------|---------|
| `list_incidents`      | read     | List declared incidents, newest declared first. Optional `status` filter; `limit` ≤ 100 and `offset`. |
| `get_incident`        | read     | One incident with its timeline (newest first, up to 50 events) and linked status report. |
| `declare_incident`    | mutation | Declare an incident with a `title` and `severity` (`critical` / `major` / `minor`), optionally a summary, commander, start time and status report to link. |
| `update_incident`     | mutation | Change the title, severity, summary, commander or start time. |
| `set_incident_status` | mutation | Move to `mitigated`, `resolved`, `canceled` or back to `open`, with an optional timeline note. Canceling closes the incident for good. |
| `resolve_incident`    | mutation | Mark an incident resolved. Does not resolve its status report; use `resolve_status_report` for that. |
| `add_incident_note`   | mutation | Append a note to the timeline. Append-only. |
| `get_postmortem`      | read     | Read the incident's postmortem (markdown), if one was drafted. |
| `draft_postmortem`    | mutation | Save a postmortem draft for a resolved incident. Replaces an existing draft; an approved postmortem can't be redrafted. |
| `approve_postmortem`  | mutation | Approve the draft and, by default, close the incident. Only an owner, an admin or the incident's commander can approve. |

### 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. |

### Workspace

| Tool            | Type | Purpose |
|-----------------|------|---------|
| `get_workspace` | read | The workspace's name, slug, plan, trial end, and usage counts (monitors, status pages, components, notification channels, status reports). Plan limits and billing identifiers 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** — public
documents 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 failure
the 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`, `add_maintenance_update` — 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 no-op and the tools report `notified: false`. 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 |
| `add_maintenance_update`   | Notification for the new note |
| `update_status_report`     | n/a — metadata-only edit, never has a notify path |
| `update_status_report_update` / `delete_status_report_update` | n/a — edits to existing entries never notify |

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

```bash
claude 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

```bash
codex mcp add openstatus --url https://api.openstatus.dev/mcp
codex 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:

```env
OAUTH_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).