diff --git a/.env.docker.example b/.env.docker.example index 19c102bdb..b7c721a0c 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -160,6 +160,13 @@ NEXT_PUBLIC_URL=http://localhost:3002 # Screenshot service (optional) SCREENSHOT_SERVICE_URL= +# OAuth for the MCP server. The API server is the authorization server and the +# dashboard serves the consent screen, so both need their public origins here. +# With NODE_ENV=production the defaults point at openstatus Cloud, which would +# send your users to app.openstatus.dev to approve a connection. +OAUTH_ISSUER=http://localhost:3001 +DASHBOARD_URL=http://localhost:3002 + # NOTE: OPENSTATUS_INGEST_URL does not belong here. No service in this compose # stack reads it -- it configures the *probe* (ghcr.io/openstatushq/private-location), # which runs on whatever host you monitor from. Set it there, alongside diff --git a/apps/dashboard/src/app/(dashboard)/agents/breadcrumb.tsx b/apps/dashboard/src/app/(dashboard)/agents/breadcrumb.tsx index 314e38dbc..c7b91a84b 100644 --- a/apps/dashboard/src/app/(dashboard)/agents/breadcrumb.tsx +++ b/apps/dashboard/src/app/(dashboard)/agents/breadcrumb.tsx @@ -1,13 +1,31 @@ "use client"; import { Agent } from "@openstatus/icons"; +import { usePathname } from "next/navigation"; import { NavBreadcrumb } from "@/components/nav/nav-breadcrumb"; +import { AGENT_TABS } from "./constants"; + export function Breadcrumb() { + const pathname = usePathname(); + const segment = pathname.split("/").pop() ?? ""; + const currentTab = AGENT_TABS.find((tab) => tab.value === segment); + return ( ); } diff --git a/apps/dashboard/src/app/(dashboard)/agents/cli/page.tsx b/apps/dashboard/src/app/(dashboard)/agents/cli/page.tsx new file mode 100644 index 000000000..e04f61e13 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/agents/cli/page.tsx @@ -0,0 +1,376 @@ +import { + Download, + FileJson, + ApiKey as KeyIcon, + Login, + Report, + StatusPage, + Terminal, +} from "@openstatus/icons"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@openstatus/ui/components/ui/tabs"; +import React from "react"; + +import { Code } from "@/components/common/code"; +import { Link } from "@/components/common/link"; +import { + Section, + SectionDescription, + SectionGroup, + SectionHeader, + SectionTitle, +} from "@/components/content/section"; + +const OS = ["macOS", "Windows", "Linux"] as const; + +const installs = [ + { + title: "Install CLI", + icon: Terminal, + description: + "Install the OpenStatus CLI to set up your monitors straight in your code.", + command: { + macOS: [ + "brew install openstatusHQ/cli/openstatus --cask", + "curl -fsSL https://raw.githubusercontent.com/openstatusHQ/cli/refs/heads/main/install.sh | bash", + ], + Linux: [ + "curl -fsSL https://raw.githubusercontent.com/openstatusHQ/cli/refs/heads/main/install.sh | bash", + ], + Windows: [ + "iwr https://raw.githubusercontent.com/openstatusHQ/cli/refs/heads/main/install.ps1| iex", + ], + }, + }, + { + title: "Login", + icon: Login, + description: "Save your API token for use in subsequent commands.", + command: "openstatus login", + }, + { + title: "Add API Key", + icon: KeyIcon, + description: ( + <> + Or set it as an environment variable. Create an API key in your + workspace settings. + + ), + command: { + macOS: ["export OPENSTATUS_API_TOKEN="], + Windows: ['$env:OPENSTATUS_API_TOKEN = ""'], + Linux: ["export OPENSTATUS_API_TOKEN="], + }, + }, + { + title: "List Status Pages", + icon: StatusPage, + description: "List all status pages in your workspace.", + command: "openstatus status-page list", + }, + { + title: "List Status Reports", + icon: Report, + description: "List all status reports in your workspace.", + command: "openstatus status-report list", + }, + { + title: "Import Monitors", + icon: Download, + description: "Import monitors from your workspace to a YAML file.", + command: "openstatus monitors import", + }, + { + title: "Manage Monitors", + icon: FileJson, + description: + "Add, remove, or update monitors from a YAML file and apply your changes.", + command: "openstatus monitors apply", + }, +] satisfies { + title: string; + icon: React.ElementType; + description: React.ReactNode; + command: string | Record<(typeof OS)[number], string[]>; +}[]; + +const commands = [ + { + command: "openstatus monitors list [options]", + description: "List all monitors in your workspace.", + }, + { + command: "openstatus monitors info [monitor-id] [options]", + description: "Get information about a specific monitor.", + }, + { + command: "openstatus monitors trigger [monitor-id] [options]", + description: "Trigger a monitor.", + }, + { + command: "openstatus status-page list [options]", + description: "List all status pages.", + }, + { + command: "openstatus status-page info [page-id]", + description: "Get status page details.", + }, + { + command: + 'openstatus status-report create --title "..." --status investigating --message "..." --page-id 123', + description: "Create a status report.", + }, + { + command: + 'openstatus status-report add-update [report-id] --status resolved --message "..."', + description: "Add an update to a status report.", + }, + { + command: "openstatus status-report list [options]", + description: "List all status reports.", + }, +]; + +const templates = [ + { + description: "MCP server", + template: `# yaml-language-server: $schema=https://www.openstatus.dev/schema.json + +mcp-server: + name: "HF MCP Server" + description: "Hugging Face MCP server monitoring" + frequency: "1m" + active: true + regions: ["iad", "ams", "lax"] + retry: 3 + kind: http + request: + url: https://hf.co/mcp + method: POST + body: > + { + "jsonrpc": "2.0", + "id": "openstatus", + "method": "ping" + } + headers: + User-Agent: OpenStatus + Accept: application/json, text/event-stream + Content-Type: application/json + assertions: + - kind: statusCode + compare: eq + target: 200 + - kind: textBody + compare: eq + target: '{"result":{},"jsonrpc":"2.0","id":"openstatus"}' +`, + }, +]; + +const githubActions = [ + { + description: "Run synthetic tests", + template: `name: OpenStatus +on: [push] + +jobs: + run-synthetic-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: OpenStatus GitHub Action + uses: openstatusHQ/openstatus-github-action@v1 + with: + api_key: \${{ secrets.OPENSTATUS_API_KEY }}`, + }, + { + description: "Apply monitors configuration", + template: `name: OpenStatus +on: [push] + +jobs: + apply-monitors: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: OpenStatus CLI Action + uses: openstatusHQ/cli-action@v1 + with: + args: monitors apply + env: + OPENSTATUS_API_TOKEN: \${{ secrets.OPENSTATUS_API_TOKEN }}`, + }, +]; + +export default function Page() { + return ( + +
+ + CLI + + Get started with the CLI to export and manage your monitors in your + code.{" "} + + Read more + + . + + + + + {OS.map((os) => ( + + {os} + + ))} + + {OS.map((os) => ( + + {installs.map((step, i) => { + const commands = + typeof step.command === "string" + ? step.command + : step.command[os]; + return ( +
+
+

+ + {step.title} +

+

+ {step.description} +

+
+ {typeof commands === "string" ? ( + {commands} + ) : ( + <> + {commands.map((command, i) => ( + + {command} + {i < commands.length - 1 && ( + or + )} + + ))} + + )} +
+ ); + })} +
+ ))} +
+
+
+ + Commands + + We have a few more commands to run. Check the{" "} + + documentation + {" "} + to read more. + + +
    + {commands.map((command, i) => ( +
  • +

    + {command.description} +

    + {command.command} +
  • + ))} +
+
+
+ + Skills + + Add the openstatus skill to let AI agents manage your monitors, + status pages, and status reports on your behalf. + + + npx skills add openstatushq/cli +
+
+ + GitHub Action + + We provide you with a github action in case you'd like to use the + CLI within your CI/CD workflows. Check the{" "} + + GitHub integration + {" "} + page or our{" "} + + guide + {" "} + to run synthetic tests in a GitHub action. + + +
+ {githubActions.map((action, i) => ( +
+

+ {action.description} +

+ {action.template} +
+ ))} +
+
+
+ + Templates + + We have a few templates to help you get started. Check the{" "} + + @openstatusHQ/cli-template + {" "} + repository for more. + + +
+ {templates.map((template, i) => ( +
+

+ {template.description} +

+ {template.template} +
+ ))} +
+
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/agents/constants.tsx b/apps/dashboard/src/app/(dashboard)/agents/constants.tsx new file mode 100644 index 000000000..bbb04f301 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/agents/constants.tsx @@ -0,0 +1,39 @@ +import type { IconProps, IconType } from "@openstatus/icons"; +import { Terminal } from "@openstatus/icons"; +import { ModelContextProtocolIcon, SlackIcon } from "@openstatus/icons/brand"; + +// brand icons take no `size` prop; map it to width/height so NavTabs/breadcrumb render them at 16px +function withSize(Icon: React.ComponentType>) { + const Sized: IconType = ({ + size = 24, + absoluteStrokeWidth: _, + ...props + }: IconProps) => ; + return Sized; +} + +export const AGENT_TABS: { + value: string; + label: string; + icon: IconType; + docs: string; +}[] = [ + { + value: "slack", + label: "Slack Agent", + icon: withSize(SlackIcon), + docs: "https://www.openstatus.dev/docs/guides/how-to-setup-slack-agent", + }, + { + value: "mcp", + label: "MCP Server", + icon: withSize(ModelContextProtocolIcon), + docs: "https://www.openstatus.dev/docs/reference/mcp-server", + }, + { + value: "cli", + label: "CLI", + icon: Terminal, + docs: "https://www.openstatus.dev/docs/reference/cli-reference", + }, +]; diff --git a/apps/dashboard/src/app/(dashboard)/agents/layout.tsx b/apps/dashboard/src/app/(dashboard)/agents/layout.tsx index affb067cb..d65dc3e35 100644 --- a/apps/dashboard/src/app/(dashboard)/agents/layout.tsx +++ b/apps/dashboard/src/app/(dashboard)/agents/layout.tsx @@ -7,6 +7,7 @@ import { AppSidebarTrigger } from "@/components/nav/app-sidebar"; import { Breadcrumb } from "./breadcrumb"; import { NavActions } from "./nav-actions"; +import { Tabs } from "./tabs"; export default function Layout({ children }: { children: React.ReactNode }) { return ( @@ -20,6 +21,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { +
{children}
); diff --git a/apps/dashboard/src/app/(dashboard)/agents/mcp/page.tsx b/apps/dashboard/src/app/(dashboard)/agents/mcp/page.tsx new file mode 100644 index 000000000..e45329d28 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/agents/mcp/page.tsx @@ -0,0 +1,351 @@ +import { Info } from "@openstatus/icons"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@openstatus/ui/components/ui/tabs"; +import NextLink from "next/link"; + +import { Code } from "@/components/common/code"; +import { Link } from "@/components/common/link"; +import { Note, NoteButton } from "@/components/common/note"; +import { + Section, + SectionDescription, + SectionGroup, + SectionHeader, + SectionTitle, +} from "@/components/content/section"; + +const ENDPOINT = "https://api.openstatus.dev/mcp"; + +const clients = [ + { + value: "claude-code", + label: "Claude Code", + description: ( + <> + Add the server, then run /mcp, select{" "} + openstatus and choose Authenticate. + Your browser opens the consent screen. + + ), + snippets: [ + { + description: "OAuth (recommended)", + code: `claude mcp add --transport http --scope user openstatus ${ENDPOINT}`, + }, + { + description: "API key (headless)", + code: `claude mcp add --transport http --scope user openstatus ${ENDPOINT} --header "x-openstatus-key: os_..."`, + }, + ], + }, + { + value: "claude", + label: "Claude.ai / Desktop", + description: ( + <> + Open Settings, Connectors,{" "} + Add custom connector, paste the URL and click{" "} + Connect. Approve on the consent screen and enable the + connector in the chat's tool picker. + + ), + snippets: [ + { description: "Server URL", code: ENDPOINT }, + { + description: "API key via mcp-remote (Claude Desktop only)", + code: `{ + "mcpServers": { + "openstatus": { + "command": "npx", + "args": [ + "mcp-remote", + "${ENDPOINT}", + "--header", + "x-openstatus-key: \${API_KEY}" + ], + "env": { + "API_KEY": "os_..." + } + } + } +}`, + }, + ], + }, + { + value: "cursor", + label: "Cursor", + description: ( + <> + Add the server to .cursor/mcp.json (project) or{" "} + ~/.cursor/mcp.json (global). Cursor shows it as{" "} + Needs login; click it to run the OAuth flow. + + ), + snippets: [ + { + description: "OAuth (recommended)", + code: `{ + "mcpServers": { + "openstatus": { + "url": "${ENDPOINT}" + } + } +}`, + }, + { + description: "API key", + code: `{ + "mcpServers": { + "openstatus": { + "url": "${ENDPOINT}", + "headers": { "x-openstatus-key": "os_..." } + } + } +}`, + }, + ], + }, + { + value: "opencode", + label: "opencode", + description: ( + <> + Add the server to opencode.json, then run{" "} + opencode mcp auth openstatus to complete the OAuth flow. + + ), + snippets: [ + { + description: "OAuth (recommended)", + code: `{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "openstatus": { + "type": "remote", + "url": "${ENDPOINT}", + "enabled": true + } + } +}`, + }, + { + description: "API key", + code: `{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "openstatus": { + "type": "remote", + "url": "${ENDPOINT}", + "enabled": true, + "headers": { "x-openstatus-key": "{env:OPENSTATUS_API_KEY}" }, + "oauth": false + } + } +}`, + }, + ], + }, + { + value: "chatgpt", + label: "ChatGPT", + description: ( + <> + Open Settings, Connectors,{" "} + Create, enter the URL as the MCP server URL with{" "} + OAuth authentication, and complete the consent screen. + + ), + snippets: [{ description: "Server URL", code: ENDPOINT }], + }, +]; + +const prompts = [ + { + description: "Check the health of a monitor before a release.", + prompt: "How did the checkout API perform over the last 7 days?", + }, + { + description: "Draft an incident. The assistant asks before notifying.", + prompt: + "Create a status report on the API status page: elevated error rates on payments, status investigating.", + }, + { + description: "Update subscribers on an ongoing incident.", + prompt: + "Post an update that we identified the cause and a fix is rolling out.", + }, + { + description: "Plan downtime ahead of time.", + prompt: + "Schedule a maintenance window for the database next Friday from 2 to 3 PM UTC.", + }, +]; + +const tools = [ + { + group: "Status pages", + items: ["list_status_pages", "list_page_components"], + }, + { + group: "Status reports", + items: [ + "list_status_reports", + "create_status_report", + "add_status_report_update", + "update_status_report", + "resolve_status_report", + ], + }, + { + group: "Maintenance", + items: ["list_maintenances", "create_maintenance"], + }, + { + group: "Monitors", + items: [ + "list_monitors", + "get_monitor", + "get_monitor_status", + "get_monitor_summary", + "list_response_logs", + "get_response_log", + ], + }, + { + group: "Workspace", + items: [ + "list_notifications", + "list_private_locations", + "list_audit_logs", + "get_audit_log", + ], + }, +]; + +export default function Page() { + return ( + +
+ + MCP Server + + Connect Claude, ChatGPT, Cursor or any Model Context Protocol client + to read and manage your status pages, reports and maintenances from + a conversation.{" "} + + Read more + + . + + + {ENDPOINT} + + + {clients.map((client) => ( + + {client.label} + + ))} + + {clients.map((client) => ( + +

+ {client.description} +

+ {client.snippets.map((snippet) => ( +
+

+ {snippet.description} +

+ {snippet.code} +
+ ))} +
+ ))} +
+
+
+ + Authentication + + OAuth is the default for interactive clients. Point the client at + the endpoint with no header, sign in, pick the workspace and access + level, and approve. Nothing to copy. + + + + + Every client you authorize via OAuth shows up under{" "} + Settings > Integrations as a connected app. Check + which apps have access and revoke them at any time. + + + View connected apps + + + +

+ For CI, cron jobs or agents running on a server, send an API key in + the x-openstatus-key header instead. Create one in the{" "} + general settings. The key's + read-only or read & write scope decides which tools the server + exposes. +

+
+
+ + Prompts + + Here are some examples of what you can ask once the server is + connected. + + + + + Publishing tools always ask whether to notify subscribers before they + run. Nothing is sent without an explicit yes. + +
    + {prompts.map((item) => ( +
  • +

    + {item.description} +

    + {item.prompt} +
  • + ))} +
+
+
+ + Tools + + Read tools are available on every credential. Mutation tools require + read & write access. Audit log tools require the audit-log + feature on your plan. + + +
    + {tools.map((tool) => ( +
  • +

    {tool.group}

    + {tool.items.join("\n")} +
  • + ))} +
+
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/agents/nav-actions.tsx b/apps/dashboard/src/app/(dashboard)/agents/nav-actions.tsx index 6702d7a64..97150ca16 100644 --- a/apps/dashboard/src/app/(dashboard)/agents/nav-actions.tsx +++ b/apps/dashboard/src/app/(dashboard)/agents/nav-actions.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Docs } from "@openstatus/icons"; import { Button } from "@openstatus/ui/components/ui/button"; import { @@ -6,10 +8,18 @@ import { TooltipProvider, TooltipTrigger, } from "@openstatus/ui/components/ui/tooltip"; +import { usePathname } from "next/navigation"; import { NavFeedback } from "@/components/nav/nav-feedback"; +import { AGENT_TABS } from "./constants"; + export function NavActions() { + const pathname = usePathname(); + const segment = pathname.split("/").pop() ?? ""; + const currentTab = AGENT_TABS.find((tab) => tab.value === segment); + const docs = currentTab?.docs ?? "https://www.openstatus.dev/docs"; + return (
@@ -17,11 +27,7 @@ export function NavActions() { diff --git a/apps/dashboard/src/app/(dashboard)/agents/page.tsx b/apps/dashboard/src/app/(dashboard)/agents/page.tsx index 1ced6883c..36049bb4a 100644 --- a/apps/dashboard/src/app/(dashboard)/agents/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/agents/page.tsx @@ -1,135 +1,5 @@ -"use client"; - -import { Info } from "@openstatus/icons"; -import { Button } from "@openstatus/ui/components/ui/button"; -import { useQuery } from "@tanstack/react-query"; - -import { Code } from "@/components/common/code"; -import { Link } from "@/components/common/link"; -import { Note } from "@/components/common/note"; -import { - SectionDescription, - SectionGroup, - SectionHeader, - SectionTitle, -} from "@/components/content/section"; -import { Section } from "@/components/content/section"; -import { useTRPC } from "@/lib/trpc/client"; - -const messages = [ - { - message: - "@openstatus create an incident for the payment API – high latency detected.", - description: "Open a new incident and notify your subscribers.", - }, - { - message: - "@openstatus keep the status page updated that we are still monitoring the issue.", - description: "Update the status report to 'Monitoring'.", - }, - { - message: "@openstatus resolve the ongoing incident on my API status page.", - description: "Close an active incident and update your subscribers.", - }, - { - message: - "@openstatus schedule a maintenance window for my database next Friday from 2–3 PM.", - description: "Plan downtime so subscribers are informed in advance.", - }, -]; - -const commands = [ - { - command: "/openstatus subscribe ", - description: "Subscribe this channel to a status page.", - }, - { - command: "/openstatus unsubscribe ", - description: "Unsubscribe this channel from a status page.", - }, - { - command: "/openstatus subscriptions", - description: "Show this channel's subscriptions.", - }, -]; +import { redirect } from "next/navigation"; export default function Page() { - const trpc = useTRPC(); - const { data: workspace } = useQuery(trpc.workspace.get.queryOptions()); - return ( - -
- - Slack agent - - Use our Slack agent to manage your status pages and incidents.{" "} - - Read more - - . - - - {!workspace?.limits["slack-agent"] ? ( - - - This is a paid feature. Upgrade your plan to use the Slack agent. - - ) : null} - -
-
- - Messages - - Here are some examples of messages that the Slack agent can handle. - - - - - Mention the @openstatus bot to trigger a response. This keeps threads - clean without the bot spamming your team. - -
    - {messages.map((message, i) => ( -
  • -

    - {message.description} -

    - {message.message} -
  • - ))} -
-
-
- - Commands - - Slash commands to manage which status pages notify this channel. - Also useful for Slack Connect channels shared with customers. - - - - - Subscribing also joins the bot to the channel. Private channels - require /invite @openstatus first. - -
    - {commands.map((command, i) => ( -
  • -

    - {command.description} -

    - {command.command} -
  • - ))} -
-
-
- ); + redirect("/agents/slack"); } diff --git a/apps/dashboard/src/app/(dashboard)/agents/slack/page.tsx b/apps/dashboard/src/app/(dashboard)/agents/slack/page.tsx new file mode 100644 index 000000000..2a4ee01a5 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/agents/slack/page.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { Info } from "@openstatus/icons"; +import { Button } from "@openstatus/ui/components/ui/button"; +import { useQuery } from "@tanstack/react-query"; + +import { Code } from "@/components/common/code"; +import { Link } from "@/components/common/link"; +import { Note } from "@/components/common/note"; +import { + SectionDescription, + SectionGroup, + SectionHeader, + SectionTitle, +} from "@/components/content/section"; +import { Section } from "@/components/content/section"; +import { useTRPC } from "@/lib/trpc/client"; + +const messages = [ + { + message: + "@openstatus create an incident for the payment API – high latency detected.", + description: + "Open a new incident, then Approve or Approve & Notify subscribers.", + }, + { + message: + "@openstatus keep the status page updated that we are still monitoring the issue.", + description: "Update the status report to 'Monitoring'.", + }, + { + message: "@openstatus resolve the ongoing incident on my API status page.", + description: + "Close an active incident, then Approve or Approve & Notify subscribers.", + }, + { + message: + "@openstatus schedule a maintenance window for my database next Friday from 2–3 PM.", + description: "Plan downtime so subscribers are informed in advance.", + }, +]; + +const commands = [ + { + command: "/openstatus subscribe ", + description: "Subscribe this channel to a status page.", + }, + { + command: "/openstatus unsubscribe ", + description: "Unsubscribe this channel from a status page.", + }, + { + command: "/openstatus subscriptions", + description: "Show this channel's subscriptions.", + }, +]; + +export default function Page() { + const trpc = useTRPC(); + const { data: workspace } = useQuery(trpc.workspace.get.queryOptions()); + return ( + +
+ + Slack Agent + + Use our Slack agent to manage your status pages and incidents.{" "} + + Read more + + . + + + {!workspace?.limits["slack-agent"] ? ( + + + This is a paid feature. Upgrade your plan to use the Slack agent. + + ) : null} + +
+
+ + Messages + + Here are some examples of messages that the Slack agent can handle. + + + + + Mention the @openstatus bot to trigger a response. This keeps threads + clean without the bot spamming your team. + +
    + {messages.map((message, i) => ( +
  • +

    + {message.description} +

    + {message.message} +
  • + ))} +
+
+
+ + Commands + + Slash commands to manage which status pages notify this channel. + Also useful for Slack Connect channels shared with customers. + + + + + Subscribing also joins the bot to the channel. Private channels + require /invite @openstatus first. + +
    + {commands.map((command, i) => ( +
  • +

    + {command.description} +

    + {command.command} +
  • + ))} +
+
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/agents/tabs.tsx b/apps/dashboard/src/app/(dashboard)/agents/tabs.tsx new file mode 100644 index 000000000..8468df6df --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/agents/tabs.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { NavTabs } from "@/components/nav/nav-tabs"; + +import { AGENT_TABS } from "./constants"; + +export function Tabs() { + return ( + ({ + ...tab, + href: `/agents/${tab.value}`, + }))} + /> + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/cli/breadcrumb.tsx b/apps/dashboard/src/app/(dashboard)/cli/breadcrumb.tsx deleted file mode 100644 index 10b4015ec..000000000 --- a/apps/dashboard/src/app/(dashboard)/cli/breadcrumb.tsx +++ /dev/null @@ -1,11 +0,0 @@ -"use client"; - -import { Terminal } from "@openstatus/icons"; - -import { NavBreadcrumb } from "@/components/nav/nav-breadcrumb"; - -export function Breadcrumb() { - return ( - - ); -} diff --git a/apps/dashboard/src/app/(dashboard)/cli/layout.tsx b/apps/dashboard/src/app/(dashboard)/cli/layout.tsx deleted file mode 100644 index affb067cb..000000000 --- a/apps/dashboard/src/app/(dashboard)/cli/layout.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { - AppHeader, - AppHeaderActions, - AppHeaderContent, -} from "@/components/nav/app-header"; -import { AppSidebarTrigger } from "@/components/nav/app-sidebar"; - -import { Breadcrumb } from "./breadcrumb"; -import { NavActions } from "./nav-actions"; - -export default function Layout({ children }: { children: React.ReactNode }) { - return ( -
- - - - - - - - - -
{children}
-
- ); -} diff --git a/apps/dashboard/src/app/(dashboard)/cli/nav-actions.tsx b/apps/dashboard/src/app/(dashboard)/cli/nav-actions.tsx deleted file mode 100644 index 6702d7a64..000000000 --- a/apps/dashboard/src/app/(dashboard)/cli/nav-actions.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Docs } from "@openstatus/icons"; -import { Button } from "@openstatus/ui/components/ui/button"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@openstatus/ui/components/ui/tooltip"; - -import { NavFeedback } from "@/components/nav/nav-feedback"; - -export function NavActions() { - return ( -
- - - - - - - View Documentation - - -
- ); -} diff --git a/apps/dashboard/src/app/(dashboard)/cli/page.tsx b/apps/dashboard/src/app/(dashboard)/cli/page.tsx index be89fa045..fbcffd060 100644 --- a/apps/dashboard/src/app/(dashboard)/cli/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/cli/page.tsx @@ -1,376 +1,5 @@ -import { - Download, - FileJson, - ApiKey as KeyIcon, - Login, - Report, - StatusPage, - Terminal, -} from "@openstatus/icons"; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@openstatus/ui/components/ui/tabs"; -import React from "react"; - -import { Code } from "@/components/common/code"; -import { Link } from "@/components/common/link"; -import { - SectionDescription, - SectionGroup, - SectionHeader, - SectionTitle, -} from "@/components/content/section"; -import { Section } from "@/components/content/section"; - -const OS = ["macOs", "Windows", "Linux"] as const; - -const installs = [ - { - title: "Install CLI", - icon: Terminal, - description: - "Install the OpenStatus CLI to set up your monitors straight in your code.", - command: { - macOs: [ - "brew install openstatusHQ/cli/openstatus --cask", - "curl -fsSL https://raw.githubusercontent.com/openstatusHQ/cli/refs/heads/main/install.sh | bash", - ], - Linux: [ - "curl -fsSL https://raw.githubusercontent.com/openstatusHQ/cli/refs/heads/main/install.sh | bash", - ], - Windows: [ - "iwr https://raw.githubusercontent.com/openstatusHQ/cli/refs/heads/main/install.ps1| iex", - ], - }, - }, - { - title: "Login", - icon: Login, - description: "Save your API token for use in subsequent commands.", - command: "openstatus login", - }, - { - title: "Add API Key", - icon: KeyIcon, - description: ( - <> - Or set it as an environment variable. Create an API key in your - workspace settings. - - ), - command: { - macOs: ["export OPENSTATUS_API_TOKEN="], - Windows: ["set OPENSTATUS_API_TOKEN="], - Linux: ["export OPENSTATUS_API_TOKEN="], - }, - }, - { - title: "List Status Pages", - icon: StatusPage, - description: "List all status pages in your workspace.", - command: "openstatus status-page list", - }, - { - title: "List Status Reports", - icon: Report, - description: "List all status reports in your workspace.", - command: "openstatus status-report list", - }, - { - title: "Import Monitors", - icon: Download, - description: "Import monitors from your workspace to a YAML file.", - command: "openstatus monitors import", - }, - { - title: "Manage Monitors", - icon: FileJson, - description: - "Add, remove, or update monitors from a YAML file and apply your changes.", - command: "openstatus monitors apply", - }, -] satisfies { - title: string; - icon: React.ElementType; - description: React.ReactNode; - command: string | Record<(typeof OS)[number], string[]>; -}[]; - -const commands = [ - { - command: "openstatus monitors list [options]", - description: "List all monitors in your workspace.", - }, - { - command: "openstatus monitors info [monitor-id] [options]", - description: "Get information about a specific monitor.", - }, - { - command: "openstatus monitors trigger [monitor-id] [options]", - description: "Trigger a monitor.", - }, - { - command: "openstatus status-page list [options]", - description: "List all status pages.", - }, - { - command: "openstatus status-page info [page-id]", - description: "Get status page details.", - }, - { - command: - 'openstatus status-report create --title "..." --status investigating --message "..." --page-id 123', - description: "Create a status report.", - }, - { - command: - 'openstatus status-report add-update [report-id] --status resolved --message "..."', - description: "Add an update to a status report.", - }, - { - command: "openstatus status-report list [options]", - description: "List all status reports.", - }, -]; - -const templates = [ - { - description: "MCP server", - template: `# yaml-language-server: $schema=https://www.openstatus.dev/schema.json - -mcp-server: - name: "HF MCP Server" - description: "Hugging Face MCP server monitoring" - frequency: "1m" - active: true - regions: ["iad", "ams", "lax"] - retry: 3 - kind: http - request: - url: https://hf.co/mcp - method: POST - body: > - { - "jsonrpc": "2.0", - "id": "openstatus", - "method": "ping" - } - headers: - User-Agent: OpenStatus - Accept: application/json, text/event-stream - Content-Type: application/json - assertions: - - kind: statusCode - compare: eq - target: 200 - - kind: textBody - compare: eq - target: '{"result":{},"jsonrpc":"2.0","id":"openstatus"}' -`, - }, -]; - -const githubActions = [ - { - description: "Run synthetic tests", - template: `name: OpenStatus -on: [push] - -jobs: - run-synthetic-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: OpenStatus GitHub Action - uses: openstatusHQ/openstatus-github-action@v1 - with: - api_key: \${{ secrets.OPENSTATUS_API_KEY }}`, - }, - { - description: "Apply monitors configuration", - template: `name: OpenStatus -on: [push] - -jobs: - apply-monitors: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: OpenStatus CLI Action - uses: openstatusHQ/cli-action@v1 - with: - args: monitors apply - env: - OPENSTATUS_API_TOKEN: \${{ secrets.OPENSTATUS_API_TOKEN }}`, - }, -]; +import { redirect } from "next/navigation"; export default function Page() { - return ( - -
- - CLI - - Get started with the CLI to export and manage your monitors in your - code.{" "} - - Read more - - . - - - - - {OS.map((os) => ( - - {os} - - ))} - - {OS.map((os) => ( - - {installs.map((step, i) => { - const commands = - typeof step.command === "string" - ? step.command - : step.command[os]; - return ( -
-
-

- - {step.title} -

-

- {step.description} -

-
- {typeof commands === "string" ? ( - {commands} - ) : ( - <> - {commands.map((command, i) => ( - - {command} - {i < commands.length - 1 && ( - or - )} - - ))} - - )} -
- ); - })} -
- ))} -
-
-
- - Commands - - We have a few more commands to run. Check the{" "} - - documentation - {" "} - to read more. - - -
    - {commands.map((command, i) => ( -
  • -

    - {command.description} -

    - {command.command} -
  • - ))} -
-
-
- - Skills - - Add the openstatus skill to let AI agents manage your monitors, - status pages, and status reports on your behalf. - - - npx skills add openstatushq/cli -
-
- - GitHub Action - - We provide you with a github action in case you'd like to use the - CLI within your CI/CD workflows. Check the{" "} - - GitHub integration - {" "} - page or our{" "} - - guide - {" "} - to to run synthetic tests in a GitHub action. - - -
- {githubActions.map((action, i) => ( -
-

- {action.description} -

- {action.template} -
- ))} -
-
-
- - Templates - - We have a few templates to help you get started. Check the{" "} - - @openstatusHQ/cli-template - {" "} - repository for more. - - -
- {templates.map((template, i) => ( -
-

- {template.description} -

- {template.template} -
- ))} -
-
-
- ); + redirect("/agents/cli"); } diff --git a/apps/dashboard/src/components/forms/components/form-import.tsx b/apps/dashboard/src/components/forms/components/form-import.tsx index 2ab39ac0c..c08b84116 100644 --- a/apps/dashboard/src/components/forms/components/form-import.tsx +++ b/apps/dashboard/src/components/forms/components/form-import.tsx @@ -397,7 +397,7 @@ export function FormImport({ Your Checkly account ID. Found in Checkly under - Account Settings → General. + Account Settings > General. )} diff --git a/apps/dashboard/src/components/nav/app-sidebar.tsx b/apps/dashboard/src/components/nav/app-sidebar.tsx index b93cee535..bbc6f5c1c 100644 --- a/apps/dashboard/src/components/nav/app-sidebar.tsx +++ b/apps/dashboard/src/components/nav/app-sidebar.tsx @@ -40,6 +40,8 @@ const data = { }, overview: [ { name: "Overview", url: NAV.overview.href, icon: NAV.overview.icon }, + { name: "Assistant", url: NAV.chat.href, icon: NAV.chat.icon }, + { name: "Agents", url: NAV.agents.href, icon: NAV.agents.icon }, { name: "Status Pages", url: NAV.statusPages.href, @@ -52,8 +54,6 @@ const data = { icon: NAV.notifications.icon, }, { name: "Settings", url: NAV.settings.href, icon: NAV.settings.icon }, - { name: "Assistant", url: NAV.chat.href, icon: NAV.chat.icon }, - { name: "Slack agent", url: NAV.agents.href, icon: NAV.agents.icon }, ], }; diff --git a/apps/dashboard/src/config/nav.ts b/apps/dashboard/src/config/nav.ts index 721992c3e..ecf9cd129 100644 --- a/apps/dashboard/src/config/nav.ts +++ b/apps/dashboard/src/config/nav.ts @@ -9,6 +9,7 @@ import { StatusPage, Terminal, } from "@openstatus/icons"; +import { ModelContextProtocolIcon } from "@openstatus/icons/brand"; export type NavItem = { label: string; @@ -53,16 +54,22 @@ export const NAV = { }, agents: { label: "Agents", - href: "/agents", + href: "/agents/slack", icon: Agent, - keywords: ["slack"], + keywords: ["slack", "cli", "mcp"], }, cli: { label: "CLI", - href: "/cli", + href: "/agents/cli", icon: Terminal, keywords: ["terminal"], }, + mcp: { + label: "MCP", + href: "/agents/mcp", + icon: ModelContextProtocolIcon, + keywords: ["model context protocol", "claude", "cursor"], + }, settings: { label: "Settings", href: "/settings/general", @@ -80,4 +87,5 @@ export const NAV_MENU_ITEMS: NavItem[] = [ NAV.chat, NAV.agents, NAV.cli, + NAV.mcp, ]; diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index d672da00f..73b55eb84 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -120,6 +120,11 @@ const nextConfig: NextConfig = { destination: "https://www.openstatus.dev/docs/:path*", permanent: true, }, + { + source: "/docs/guides/how-to-connect-openstatus-to-claude-code", + destination: "/docs/guides/how-to-connect-openstatus-to-your-agent", + permanent: true, + }, { source: "/legal/terms", destination: "/terms", @@ -202,7 +207,7 @@ const nextConfig: NextConfig = { }, { source: "/docs/tutorial/how-to-connect-openstatus-to-claude-code", - destination: "/docs/guides/how-to-connect-openstatus-to-claude-code", + destination: "/docs/guides/how-to-connect-openstatus-to-your-agent", permanent: true, }, { diff --git a/apps/web/public/assets/changelog/mcp-oauth.png b/apps/web/public/assets/changelog/mcp-oauth.png new file mode 100644 index 0000000000000000000000000000000000000000..e249e187b51c58f6af8a7fcc3bcf36dcd702800f GIT binary patch literal 122030 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/onS18D2ufI4adf0T5I8(TOY0S1 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 literal 0 HcmV?d00001 diff --git a/apps/web/src/app/.well-known/mcp/server-card.json/route.ts b/apps/web/src/app/.well-known/mcp/server-card.json/route.ts index fe03f6c13..51e422459 100644 --- a/apps/web/src/app/.well-known/mcp/server-card.json/route.ts +++ b/apps/web/src/app/.well-known/mcp/server-card.json/route.ts @@ -22,12 +22,14 @@ const card = { type: "streamable-http", url: "https://api.openstatus.dev/mcp", }, + // The draft schema has no settled shape for OAuth yet, so the card keeps + // the apiKey form and points at the discovery document in prose. authentication: { type: "apiKey", in: "header", name: "x-openstatus-key", description: - "Workspace API key. Generate one in Settings → API Tokens. Same credential used by the CLI, REST API, and Terraform provider. Required for tools; the resources below are readable without one.", + "OAuth 2.1 is the default: an unauthenticated request answers 401 with WWW-Authenticate, and authorization-server metadata lives at https://api.openstatus.dev/.well-known/oauth-authorization-server (dynamic client registration, PKCE, scopes read and write). The x-openstatus-key header remains for CI and headless agents, using the same API key as the CLI, REST API, and Terraform provider (Settings → API Tokens). Credentials are required for tools; the resources below are readable without one.", }, capabilities: { tools: { listChanged: false }, diff --git a/apps/web/src/content/docs.config.ts b/apps/web/src/content/docs.config.ts index 6d8a890e5..82bbb131a 100644 --- a/apps/web/src/content/docs.config.ts +++ b/apps/web/src/content/docs.config.ts @@ -1,6 +1,7 @@ // Single source of truth for docs section order, page order, and sidebar labels. -// Labels mirror each page's `title`, except section-landing pages which keep a -// short label (e.g. title "Foundational Concepts" → label "Overview"). Page→section +// Labels mirror each page's `title` in title case, except: section-landing pages +// keep a short label (e.g. title "Foundational Concepts" → label "Overview"), and +// Guides entries carry a "How to " prefix the page title may omit. Page→section // membership is mirrored by each doc's `category` frontmatter and cross-checked // at build time (see `validateDocsNav`). @@ -153,8 +154,8 @@ export const docsNav: DocsNavSection[] = [ label: "How to Auto-Post Status Updates to X and Bluesky", }, { - slug: "guides/how-to-connect-openstatus-to-claude-code", - label: "How to Connect openstatus to Claude Code", + slug: "guides/how-to-connect-openstatus-to-your-agent", + label: "How to Connect openstatus to Your Coding Agent", }, { slug: "guides/how-to-manage-openstatus-with-terraform", diff --git a/apps/web/src/content/pages/blog/from-clickops-to-chatops.mdx b/apps/web/src/content/pages/blog/from-clickops-to-chatops.mdx index 9bb1f4333..6f4e42263 100644 --- a/apps/web/src/content/pages/blog/from-clickops-to-chatops.mdx +++ b/apps/web/src/content/pages/blog/from-clickops-to-chatops.mdx @@ -11,7 +11,7 @@ Dashboards are great until you've clicked the same four buttons thirty times thi It lives at `/chat` in the dashboard. Type. Hit enter. Watch openstatus assistant do the clicking. -Or anywhere else you already chat — the same tools are exposed over [MCP](/tooling/mcp-server), so you can drive openstatus from [Claude Desktop](/guides/connect-openstatus-to-claude-desktop), [Claude Code](/guides/connect-openstatus-to-claude-code), Cursor, ChatGPT, or any other MCP-compatible client. One tool registry, two front doors. +Or anywhere else you already chat — the same tools are exposed over [MCP](/tooling/mcp-server), so you can drive openstatus from [Claude Desktop](/guides/connect-openstatus-to-claude-desktop), [Claude Code](/guides/connect-openstatus-to-claude-code), [ChatGPT](/guides/connect-openstatus-to-chatgpt), [Codex](/guides/connect-openstatus-to-codex), [opencode](/guides/connect-openstatus-to-opencode), Cursor, or any other MCP-compatible client. One tool registry, two front doors. > **PS:** if you're allergic to clicking _and_ chatting, we also have [Terraform](/tooling/terraform) for full GitOps. ClickOps · ChatOps · GitOps — the trifecta is complete. @@ -161,6 +161,9 @@ Step-by-step setup guides: - [Connect openstatus to Claude Desktop](/guides/connect-openstatus-to-claude-desktop) - [Connect openstatus to Claude Code](/guides/connect-openstatus-to-claude-code) +- [Connect openstatus to ChatGPT](/guides/connect-openstatus-to-chatgpt) +- [Connect openstatus to Codex](/guides/connect-openstatus-to-codex) +- [Connect openstatus to opencode](/guides/connect-openstatus-to-opencode) Anything else that speaks MCP — Cursor, ChatGPT custom connectors, Zed, LangChain, your own rig — works the same way. @@ -168,7 +171,7 @@ The chat UI is the reference frontend. The agent layer is the product. ## Try it -Open the dashboard → **Assistant** in the sidebar. Ask it something obvious first (_"list my monitors"_). Then ask it something you'd normally have to click for. +Open the dashboard > **Assistant** in the sidebar. Ask it something obvious first (_"list my monitors"_). Then ask it something you'd normally have to click for. The dashboard you already pay for, now with a mouth. We think you'll like it. diff --git a/apps/web/src/content/pages/changelog/api-key-scopes.mdx b/apps/web/src/content/pages/changelog/api-key-scopes.mdx index 525e4988a..54768653b 100644 --- a/apps/web/src/content/pages/changelog/api-key-scopes.mdx +++ b/apps/web/src/content/pages/changelog/api-key-scopes.mdx @@ -9,4 +9,4 @@ author: "openstatus" API keys now carry a `read` or `write` scope. Read-only keys can list and inspect resources but cannot mutate anything — useful for dashboards, read replicas, and AI agents you don't fully trust. -Pick a scope when creating a key in `Settings → API Keys`. Existing keys keep full access; rotate them to scope down. +Pick a scope when creating a key in `Settings > API Keys`. Existing keys keep full access; rotate them to scope down. diff --git a/apps/web/src/content/pages/changelog/mcp-oauth.mdx b/apps/web/src/content/pages/changelog/mcp-oauth.mdx new file mode 100644 index 000000000..adf19bee9 --- /dev/null +++ b/apps/web/src/content/pages/changelog/mcp-oauth.mdx @@ -0,0 +1,18 @@ +--- +title: "OAuth for the MCP server" +description: "Connect Claude, ChatGPT, Cursor, opencode, and any MCP client with a consent screen instead of an API key." +image: "/assets/changelog/mcp-oauth.png" +publishedAt: "2026-09-08" +category: "integrations" +author: "openstatus" +--- + +The openstatus MCP server now signs clients in with **OAuth 2.1**. Add `https://api.openstatus.dev/mcp` to Claude, ChatGPT, Cursor, Claude Code, or opencode, and the client opens a consent screen instead of asking for a key. + +On that screen you pick the **workspace**, choose **read-only or read & write**, and approve. Tokens are short-lived and refresh on their own. + +Every connection is listed under **Settings > Integrations > Connected apps**, where you can see who connected it and revoke it at any time. + +The `x-openstatus-key` header keeps working and stays the right choice for CI and headless agents. + +Details in the [MCP server reference](/docs/reference/mcp-server#oauth-endpoints), and guides for [Claude Code](/guides/connect-openstatus-to-claude-code), [Claude Desktop](/guides/connect-openstatus-to-claude-desktop), [Codex](/guides/connect-openstatus-to-codex), [ChatGPT](/guides/connect-openstatus-to-chatgpt), and [opencode](/guides/connect-openstatus-to-opencode). diff --git a/apps/web/src/content/pages/changelog/saml-sso.mdx b/apps/web/src/content/pages/changelog/saml-sso.mdx index 5868c8b65..0e87437a0 100644 --- a/apps/web/src/content/pages/changelog/saml-sso.mdx +++ b/apps/web/src/content/pages/changelog/saml-sso.mdx @@ -9,7 +9,7 @@ image: "/assets/changelog/sso.png" Your team can now sign in to openstatus through your own identity provider. **Okta, Entra ID, Google Workspace, OneLogin**, and generic SAML are all supported. -Head to **Settings → SSO**, enable it, and verify your domain with a DNS TXT record. Verification is not optional: openstatus only accepts an SSO sign-in when the email your identity provider asserts belongs to a domain you own. Then connect your provider and you're done. +Head to **Settings > SSO**, enable it, and verify your domain with a DNS TXT record. Verification is not optional: openstatus only accepts an SSO sign-in when the email your identity provider asserts belongs to a domain you own. Then connect your provider and you're done. On the login page your team selects **Sign in with SSO** and enters their work email — they're redirected to your identity provider and added to the workspace as members on first login. Signing in from your provider's app tile works too. Anyone who already has an openstatus account with the same email keeps it, along with their role. diff --git a/apps/web/src/content/pages/docs/guides/how-deploy-status-page-cf-pages.mdx b/apps/web/src/content/pages/docs/guides/how-deploy-status-page-cf-pages.mdx index 63030798c..a3bf8f6fb 100644 --- a/apps/web/src/content/pages/docs/guides/how-deploy-status-page-cf-pages.mdx +++ b/apps/web/src/content/pages/docs/guides/how-deploy-status-page-cf-pages.mdx @@ -33,7 +33,7 @@ The code for the template is available on [GitHub](https://github.com/openstatus First, you need an API key to fetch your monitoring data. 1. Navigate to your openstatus dashboard. -2. Go to **Settings** → **General** and find the **API Keys** card. +2. Go to **Settings** > **General** and find the **API Keys** card. 3. Click **Create** and copy the key. ### 2. Set up the Astro project diff --git a/apps/web/src/content/pages/docs/guides/how-to-configure-status-page.mdx b/apps/web/src/content/pages/docs/guides/how-to-configure-status-page.mdx index d85dd5529..fd9d17fc3 100644 --- a/apps/web/src/content/pages/docs/guides/how-to-configure-status-page.mdx +++ b/apps/web/src/content/pages/docs/guides/how-to-configure-status-page.mdx @@ -83,7 +83,7 @@ Example of **manual bar** with **simple card** and **hiding uptime** ### 2. Appearance -Pick a theme under **Settings → Appearance**, in the **Style** field. The shipped set is: +Pick a theme under **Settings > Appearance**, in the **Style** field. The shipped set is: - `default` (openstatus) - `default-rounded` diff --git a/apps/web/src/content/pages/docs/guides/how-to-connect-openstatus-to-claude-code.mdx b/apps/web/src/content/pages/docs/guides/how-to-connect-openstatus-to-claude-code.mdx deleted file mode 100644 index d86d4f74e..000000000 --- a/apps/web/src/content/pages/docs/guides/how-to-connect-openstatus-to-claude-code.mdx +++ /dev/null @@ -1,131 +0,0 @@ ---- -category: Guides -title: Connect openstatus to Claude Code -description: "Step-by-step tutorial to add the openstatus MCP server to Claude Code so the agent can read and manage your status pages from the terminal" ---- - - -| | | -|---|---| -| **Time** | ~3 minutes | -| **Level** | Beginner | -| **Prerequisites** | openstatus account, [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) installed and signed in | - -In this guide, you'll connect the openstatus MCP server to Claude Code so the agent can list your status pages, draft incidents, and schedule maintenance windows directly from your terminal. - -## 1. Create an API key - -Open the openstatus dashboard, go to **Settings → General**, find the **API Keys** card, and click **Create**. - -Pick a scope: - -- **Read-only** — Claude Code can list status pages, reports, and maintenance windows but cannot create or modify anything. Best for exploration and on-call summaries. -- **Read & write** — Claude Code can also create status reports, append updates, resolve incidents, and schedule maintenance. - - - -Copy the key (it starts with `os_`). You'll only see it once. - -## 2. Add the MCP server to Claude Code - -Claude Code talks to the openstatus MCP server over Streamable HTTP. Register it with the `claude mcp add` command: - -```bash -claude mcp add \ - --transport http \ - --scope user \ - --header "x-openstatus-key: os_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ - openstatus \ - https://api.openstatus.dev/mcp -``` - -The flags: - -- `--transport http` — the openstatus MCP server is a stateless HTTP endpoint, not a local stdio process. -- `--scope user` — registers the server for every Claude Code session on your machine. Use `--scope project` if you want to commit a project-local `.mcp.json` so your team picks it up automatically (omit the header from the committed file and rely on `${OPENSTATUS_API_KEY}` expansion). -- `--header "x-openstatus-key: …"` — the same API key the openstatus CLI, REST API, and Terraform provider use. - -### Project-scoped alternative - -If you'd rather commit the configuration to your repo, create a `.mcp.json` at the project root: - -```json -{ - "mcpServers": { - "openstatus": { - "type": "http", - "url": "https://api.openstatus.dev/mcp", - "headers": { - "x-openstatus-key": "${OPENSTATUS_API_KEY}" - } - } - } -} -``` - -Then export `OPENSTATUS_API_KEY` in your shell. Claude Code expands the variable at connection time, so the key never lands in version control. - -## 3. Verify the connection - -Open Claude Code and run: - -``` -/mcp -``` - -You should see `openstatus` listed as **connected**, with the registered read tools (`list_status_pages`, `list_monitors`, `get_monitor_status`, `list_private_locations`, …) and — if your key has write scope — the mutation tools (`create_status_report`, `add_status_report_update`, `resolve_status_report`, `create_maintenance`, …). See the [MCP server reference](/docs/reference/mcp-server) for the full list. - -If the server shows as disconnected, see [Troubleshooting](#troubleshooting) below. - -## 4. Try your first prompt - -Ask Claude Code something that exercises a read tool: - -``` -> list my openstatus status pages -``` - -Claude Code will call `list_status_pages`, prompt you to approve the call, and return the page slugs and ids in your workspace. - -If you have a write-scoped key, try drafting an incident — Claude Code will show you the title, status, message, and notify choice **before** firing the tool, so nothing reaches subscribers without an explicit approval: - -``` -> draft a status report on my "api" page: investigating elevated latency on the payment endpoint -``` - - - -## What you've accomplished - -- Created an openstatus API key scoped for Claude Code -- Registered the openstatus MCP server with `claude mcp add` -- Verified the connection with `/mcp` -- Ran your first read and (optionally) write tool calls - -## Troubleshooting - -### `/mcp` shows openstatus as disconnected - -1. **Re-check the header** — run `claude mcp list` and confirm the `x-openstatus-key` value matches a key visible in **Settings → General → API Keys**. Whitespace and trailing newlines are common copy-paste failures. -2. **Reachability** — the endpoint is stateless and speaks Streamable HTTP, so a plain `curl` GET returns `406 Not Acceptable` unless you ask for an event stream: - - ```bash - curl -i -H "x-openstatus-key: os_..." -H "Accept: text/event-stream" https://api.openstatus.dev/mcp - ``` - - A `401` means the key is wrong or revoked; a network error means a proxy or firewall is blocking the connection. There is no `mcp-session-id` header — the server never issues one. -3. **Re-add the server** — `claude mcp remove openstatus` then re-run the `claude mcp add` command from step 2. - -### "Tool not available" when asking Claude Code to create an incident - -Your API key is read-only. Claude Code only sees the read tools — the server filters mutation tools out of `tools/list` for read-only keys. Issue a new key with **Read & write** scope and re-add the server. - -### Audit log shows `actor_type = 'mcp'` for changes I made manually - -That's expected — every mutation routed through Claude Code (or any other MCP client) is attributed to `mcp` in the audit log, with `actor_id` set to the API key id and `actor_user_id` set to the user who created the key. This is how openstatus separates AI-driven actions from CLI, dashboard, and API mutations. - -## What's next - -- **[MCP server reference](/docs/reference/mcp-server)** — full tool schema, error codes, and the contract behind the required `notify` flag. -- **[Connect to other MCP clients](/docs/reference/mcp-server#configure-claude-desktop)** — the same API key works for Claude Desktop, Cursor, and any other MCP-compatible client. -- **[Get started with the openstatus CLI](/docs/tutorial/get-started-with-openstatus-cli)** — for terminal workflows that don't need an LLM in the loop. diff --git a/apps/web/src/content/pages/docs/guides/how-to-connect-openstatus-to-your-agent.mdx b/apps/web/src/content/pages/docs/guides/how-to-connect-openstatus-to-your-agent.mdx new file mode 100644 index 000000000..45cc94d89 --- /dev/null +++ b/apps/web/src/content/pages/docs/guides/how-to-connect-openstatus-to-your-agent.mdx @@ -0,0 +1,228 @@ +--- +category: Guides +title: Connect openstatus to your coding agent +description: "Step-by-step tutorial to add the openstatus MCP server to Claude Code, Codex, opencode, Cursor, Claude Desktop, or ChatGPT so the agent can read and manage your status pages" +--- + + +| | | +|---|---| +| **Time** | ~2 minutes | +| **Level** | Beginner | +| **Prerequisites** | openstatus account with at least one status page, an MCP-capable agent installed | + +openstatus exposes one [Model Context Protocol](https://modelcontextprotocol.io) server at `https://api.openstatus.dev/mcp`. Every MCP-capable agent connects to the same endpoint, signs in through the same OAuth consent screen, and gets the same tools — so this guide is one walkthrough with a per-agent registration step. + +By the end, your agent can list status pages, draft incidents, and schedule maintenance windows. There is no key to copy. + +## 1. Register the MCP server + +Pick your agent. The endpoint is a stateless Streamable HTTP server, not a local stdio process — some clients need to be told that explicitly. + +### Claude Code + +```bash +claude mcp add \ + --transport http \ + --scope user \ + openstatus \ + https://api.openstatus.dev/mcp +``` + +`--scope user` registers the server for every session on your machine. Use `--scope project` to write a `.mcp.json` you can commit so your team picks it up automatically; each teammate signs in on their own. + +### Codex + +```bash +codex mcp add openstatus --url https://api.openstatus.dev/mcp +``` + +`--url` marks the server as remote. The command writes to `~/.codex/config.toml`, which the Codex CLI, the Codex IDE extension, and the ChatGPT desktop app all read. + +### opencode + +Add the server to `opencode.json` (project) or `~/.config/opencode/opencode.json` (global): + +```json +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "openstatus": { + "type": "remote", + "url": "https://api.openstatus.dev/mcp", + "enabled": true + } + } +} +``` + +### Cursor + +Add the server to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global): + +```json +{ + "mcpServers": { + "openstatus": { + "url": "https://api.openstatus.dev/mcp" + } + } +} +``` + +### Claude Desktop and Claude.ai + +Both share one connector list. Open **Settings > Connectors > Add custom connector**, enter `https://api.openstatus.dev/mcp` as the URL, and click **Connect**. + +### ChatGPT + +Switch on **Developer mode** under **Settings > Security and login**, then open **Settings > Connectors > Create** and enter `https://api.openstatus.dev/mcp` as the MCP server URL with **OAuth** authentication. + +## 2. Sign in + +Trigger the OAuth flow the way your agent expects: + +| Agent | How to sign in | +|---|---| +| Claude Code | Run `/mcp`, select **openstatus**, choose **Authenticate** | +| Codex | Run `codex mcp login openstatus` | +| opencode | Run `opencode mcp auth openstatus` | +| Cursor | The server shows as **Needs login** — click it | +| Claude Desktop, Claude.ai, ChatGPT | The connect button opens the flow directly | + +Your browser opens the openstatus consent screen: + +- **Workspace** — the workspace this connection can reach. Pick one per connection. +- **Access** — **Read-only** lets the agent list status pages, reports, monitors, and maintenance windows but change nothing. **Read & write** also lets it create status reports, append updates, resolve incidents, and schedule maintenance. + +Click **Approve**. The browser hands a code back to the agent, which exchanges it for a token and refreshes it in the background from then on. + + + +## 3. Verify the connection + +Ask your agent to list its MCP servers — `/mcp` in Claude Code and Codex, `opencode mcp list`, the MCP pane in Cursor's settings, the tool picker in Claude Desktop and ChatGPT. `openstatus` should show as connected, with the read tools (`list_status_pages`, `list_monitors`, `get_monitor_status`, `list_private_locations`, …) and — if you granted write access — the mutation tools (`create_status_report`, `add_status_report_update`, `resolve_status_report`, `create_maintenance`, …). See the [MCP server reference](/docs/reference/mcp-server) for the full list. + +If the server shows as disconnected, see [Troubleshooting](#troubleshooting) below. + +## 4. Try your first prompt + +Ask for something that exercises a read tool: + +``` +> list my openstatus status pages +``` + +The agent calls `list_status_pages`, prompts you to approve the call, and returns the page slugs and ids in your workspace. + +If you granted write access, try drafting an incident — the agent shows you the title, status, message, and notify choice **before** firing the tool, so nothing reaches subscribers without an explicit approval: + +``` +> draft a status report on my "api" page: investigating elevated latency on the payment endpoint +``` + + + +## Using an API key instead + +OAuth needs a browser. For CI, a remote box, or a committed config where you'd rather not sign in, pass an API key in the `x-openstatus-key` header instead. Create one in **Settings > API Tokens** with the scope you want (scopes are immutable on a key), then add the header where your agent puts it: + +```bash +# Claude Code +claude mcp add --transport http --scope user openstatus \ + https://api.openstatus.dev/mcp \ + --header "x-openstatus-key: os_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + +# Codex +codex mcp add openstatus --url https://api.openstatus.dev/mcp \ + --header "x-openstatus-key=os_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +``` + +For the JSON- and TOML-configured clients, add the header to the server entry — `"headers": { "x-openstatus-key": "os_…" }` in `.mcp.json` and `.cursor/mcp.json`, or `env_http_headers = { "x-openstatus-key" = "OPENSTATUS_API_KEY" }` in `.codex/config.toml`. In a committed file, read the key from the environment so it never lands in version control: + +```json +{ + "mcpServers": { + "openstatus": { + "type": "http", + "url": "https://api.openstatus.dev/mcp", + "headers": { + "x-openstatus-key": "${OPENSTATUS_API_KEY}" + } + } + } +} +``` + +opencode uses its own interpolation syntax and needs OAuth switched off explicitly, otherwise it still opens a browser when a call is rejected: + +```json +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "openstatus": { + "type": "remote", + "url": "https://api.openstatus.dev/mcp", + "enabled": true, + "headers": { + "x-openstatus-key": "{env:OPENSTATUS_API_KEY}" + }, + "oauth": false + } + } +} +``` + +When a header is present, the agent skips the OAuth sign-in. + +## Managing the connection + +Every OAuth connection is listed in **Settings > Integrations > Connected apps** with the client name, granted access, who connected it, and when it was last used. Revoke it there and the agent's next call fails with `401`; sign in again to reconnect. Members can revoke their own connections; owners and admins can revoke any. Removing a member from the workspace revokes their connections automatically. + +## What you've accomplished + +- Registered the openstatus MCP server with your agent +- Signed in with OAuth and picked a workspace and access level +- Verified the connection and the tools it exposes +- Ran your first read and (optionally) write tool calls + +## Troubleshooting + +### The server shows as disconnected + +1. **Sign in again** — tokens refresh automatically, but a connection revoked from the dashboard or a member removal needs a new sign-in. +2. **Reachability** — the endpoint is stateless and speaks Streamable HTTP, so a plain `curl` GET returns `406 Not Acceptable` unless you ask for an event stream: + + ```bash + curl -i -H "Accept: text/event-stream" https://api.openstatus.dev/mcp + ``` + + A `401` with a `WWW-Authenticate` header is the expected answer for an unauthenticated request; a network error means a proxy or firewall is blocking the connection. There is no `mcp-session-id` header — the server never issues one. +3. **Re-add the server** — remove and re-register it (`claude mcp remove openstatus`, `codex mcp remove openstatus`, or delete the entry from the JSON config), then repeat step 1. + +### The browser never opens + +CLI agents print the authorization URL in the terminal. Copy it into a browser on the same machine; the callback goes to a `localhost` port the agent is listening on. If the session is remote (SSH, a container), use an [API key](#using-an-api-key-instead) instead. + +### "Tool not available" when asking the agent to create an incident + +Your connection is read-only. The agent only sees the read tools — the server filters mutation tools out of `tools/list` for read-only credentials. Sign in again and pick **Read & write** on the consent screen, or issue a write-scoped API key. + +### Audit log shows `actor_type = 'mcp'` for changes I made manually + +That's expected — every mutation routed through an MCP client is attributed to `mcp` in the audit log, with `actor_id` set to the connection (`oat_` for OAuth, the key id for an API key) and `actor_user_id` set to the user who approved the connection or created the key. This is how openstatus separates AI-driven actions from CLI, dashboard, and API mutations. + +## What's next + +- **[MCP server reference](/docs/reference/mcp-server)** — full tool schema, error codes, OAuth endpoints, and the contract behind the required `notify` flag. +- **[Get started with the openstatus CLI](/docs/tutorial/get-started-with-openstatus-cli)** — for terminal workflows that don't need an LLM in the loop. + +### Per-agent walkthroughs + +Longer guides with the safeguards, the incident workflow, and an FAQ for each client: + +- [Connect openstatus to Claude Code](/guides/connect-openstatus-to-claude-code) +- [Connect openstatus to Claude Desktop](/guides/connect-openstatus-to-claude-desktop) +- [Connect openstatus to Codex](/guides/connect-openstatus-to-codex) +- [Connect openstatus to opencode](/guides/connect-openstatus-to-opencode) +- [Connect openstatus to ChatGPT](/guides/connect-openstatus-to-chatgpt) diff --git a/apps/web/src/content/pages/docs/guides/how-to-deploy-statuspage-socials-notifier.mdx b/apps/web/src/content/pages/docs/guides/how-to-deploy-statuspage-socials-notifier.mdx index 4b2b421ed..92fb6b5fa 100644 --- a/apps/web/src/content/pages/docs/guides/how-to-deploy-statuspage-socials-notifier.mdx +++ b/apps/web/src/content/pages/docs/guides/how-to-deploy-statuspage-socials-notifier.mdx @@ -129,8 +129,8 @@ Fork or clone the [repository](https://github.com/openstatusHQ/statuspage-social #### Vercel -1. In Vercel, click **Add New → Project** and import your fork. -2. Add every variable from the table above under **Settings → Environment Variables**. +1. In Vercel, click **Add New > Project** and import your fork. +2. Add every variable from the table above under **Settings > Environment Variables**. 3. Click **Deploy**. Your webhook endpoint will be available at `https://.vercel.app/webhook`. #### Fly.io diff --git a/apps/web/src/content/pages/docs/guides/how-to-import-status-page.mdx b/apps/web/src/content/pages/docs/guides/how-to-import-status-page.mdx index 5ec1313f9..f0796e0b0 100644 --- a/apps/web/src/content/pages/docs/guides/how-to-import-status-page.mdx +++ b/apps/web/src/content/pages/docs/guides/how-to-import-status-page.mdx @@ -35,7 +35,7 @@ Where to find your API key varies by provider. See the provider-specific guides - **Statuspage** — organisation-level API key in your Atlassian account settings. - **Better Stack** — API token from the Better Stack dashboard under Integrations. - **Instatus** — API key from your Instatus workspace settings. -- **Checkly** — API key from User Settings → API keys, plus your account ID from Account Settings → General (both required). +- **Checkly** — API key from User Settings > API keys, plus your account ID from Account Settings > General (both required). ### 2. Open the import page diff --git a/apps/web/src/content/pages/docs/guides/how-to-run-synthetic-test-github-action.mdx b/apps/web/src/content/pages/docs/guides/how-to-run-synthetic-test-github-action.mdx index c1a981ee8..9c10cb767 100644 --- a/apps/web/src/content/pages/docs/guides/how-to-run-synthetic-test-github-action.mdx +++ b/apps/web/src/content/pages/docs/guides/how-to-run-synthetic-test-github-action.mdx @@ -54,7 +54,7 @@ tests: Secure your API key as a GitHub secret: 1. Go to your GitHub repository -2. Click **Settings** → **Secrets and variables** → **Actions** +2. Click **Settings** > **Secrets and variables** > **Actions** 3. Click **New repository secret** 4. Name: `OPENSTATUS_API_KEY` 5. Value: Your openstatus API key diff --git a/apps/web/src/content/pages/docs/guides/how-to-set-up-saml-sso.mdx b/apps/web/src/content/pages/docs/guides/how-to-set-up-saml-sso.mdx index cfda9d5c4..f7533d99d 100644 --- a/apps/web/src/content/pages/docs/guides/how-to-set-up-saml-sso.mdx +++ b/apps/web/src/content/pages/docs/guides/how-to-set-up-saml-sso.mdx @@ -17,11 +17,11 @@ SAML single sign-on lets your team sign in to openstatus through your own identi ## Step 1: add SSO to your workspace -Go to **Settings → Billing** and add **SAML Single Sign-On** to your subscription. Until the add-on is active, **Enable SSO** is refused. +Go to **Settings > Billing** and add **SAML Single Sign-On** to your subscription. Until the add-on is active, **Enable SSO** is refused. ## Step 2: enable SSO -Go to **Settings → SSO** and select **Enable SSO**. This creates the organization that holds your connection and verified domains. +Go to **Settings > SSO** and select **Enable SSO**. This creates the organization that holds your connection and verified domains. Only workspace owners can see and manage this page. @@ -57,4 +57,4 @@ Because membership is granted by your identity provider, removing someone from t **Disable SSO** on the settings page stops SSO sign-ins. Nobody loses access to the workspace and no memberships are removed — everyone continues with GitHub or Google. -Disabling SSO does not cancel the add-on. To stop being billed for it, remove **SAML Single Sign-On** from your subscription in **Settings → Billing**. +Disabling SSO does not cancel the add-on. To stop being billed for it, remove **SAML Single Sign-On** from your subscription in **Settings > Billing**. diff --git a/apps/web/src/content/pages/docs/guides/self-hosting-openstatus.mdx b/apps/web/src/content/pages/docs/guides/self-hosting-openstatus.mdx index 5d4f940a8..da7f9e562 100644 --- a/apps/web/src/content/pages/docs/guides/self-hosting-openstatus.mdx +++ b/apps/web/src/content/pages/docs/guides/self-hosting-openstatus.mdx @@ -83,6 +83,15 @@ This guide is divided into three parts: launching the services, setting up analy You'll fill in the Tinybird tokens in Part 2, once the container is running. + If you plan to connect MCP clients (Claude, Cursor, ChatGPT) with OAuth, also set the two origins the flow redirects through. With `NODE_ENV=production` they default to openstatus Cloud, which would send your users to `app.openstatus.dev` to approve a connection: + ```env + # Public origin of the API server (authorization server and /mcp resource). + OAUTH_ISSUER=http://localhost:3001 + # Public origin of the dashboard, which serves the consent screen. + DASHBOARD_URL=http://localhost:3002 + ``` + Use your real hostnames once the stack sits behind a reverse proxy. The [MCP server reference](/docs/reference/mcp-server#self-hosting) has the details. + 3. **Build and start services** Use Docker Compose to build and run all openstatus services in the background. @@ -217,7 +226,7 @@ Now that the services are running, you can access the dashboard and perform the The self-hosted version relies on private locations to perform checks. The ingest server is already running from Part 1 — what you deploy here is the **probe**. - - In the dashboard, navigate to **Settings → Private Locations** and create a new one. + - In the dashboard, navigate to **Settings > Private Locations** and create a new one. - Copy the generated key. - Run the probe wherever you want to check from: @@ -443,7 +452,7 @@ Or simply `docker compose up -d db-migrate`, since the step is idempotent. **Probe runs but nothing appears in the dashboard** — check, in order: 1. `OPENSTATUS_INGEST_URL` points at port **8081** (the ingest server), not 3001. -2. `OPENSTATUS_KEY` matches the key shown in **Settings → Private Locations**. +2. `OPENSTATUS_KEY` matches the key shown in **Settings > Private Locations**. 3. The ingest server logs show `IngestHTTP` requests arriving — `docker compose logs private-location`. 4. Those log lines show `tinybird.success=true`. If not, revisit step 7. diff --git a/apps/web/src/content/pages/docs/reference/mcp-server.mdx b/apps/web/src/content/pages/docs/reference/mcp-server.mdx index 420448f43..4ab766b3e 100644 --- a/apps/web/src/content/pages/docs/reference/mcp-server.mdx +++ b/apps/web/src/content/pages/docs/reference/mcp-server.mdx @@ -4,7 +4,7 @@ title: MCP Server description: Connect openstatus to AI assistants via the Model Context Protocol --- -The openstatus MCP server lets AI assistants (Claude Desktop, 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. +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 @@ -16,28 +16,53 @@ The transport is **Streamable HTTP** (stateless). The server speaks JSON-RPC 2.0 ## Authentication -Every request must include your openstatus API key in the `x-openstatus-key` header — the same key REST and ConnectRPC use. There is no separate MCP-only credential. +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 → API Keys**. +Get a key from the dashboard's **Settings > API Tokens**. When both credentials are present, the header wins. ### Scopes -Each API key carries a scope that controls what tools the MCP server exposes: +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. -Scope is set at key creation time and is **immutable**. To change a key's scope, revoke it and issue a new one. +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 — issue a **Read-only** key. 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. +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 19 tools (17 on plans without the `audit-log` feature), grouped by resource and scoped to the workspace tied to your API key. Mutations write to the audit log with `actor_type = "mcp"` (see [Audit log](#audit-log)). +The server exposes 19 tools (17 on plans without the `audit-log` feature), grouped by resource and scoped to the workspace tied to your credential. Mutations write to the audit log with `actor_type = "mcp"` (see [Audit log](#audit-log)). ### Pages @@ -130,11 +155,21 @@ The tool's response includes a `notified: boolean` field so the assistant can co `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 Desktop +## 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). -Claude Desktop launches MCP servers as local stdio processes, so the remote HTTP server is bridged through [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) — a small open-source proxy that translates Claude Desktop's stdio calls into HTTP requests against `https://api.openstatus.dev/mcp`. You'll need Node.js 18+ on your PATH so Claude Desktop can run `npx`. +For a headless setup, pass the API key instead: `--header "x-openstatus-key: os_…"`. -Add the server to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\\Claude\\claude_desktop_config.json` (Windows): +## 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 { @@ -155,26 +190,86 @@ Add the server to `~/Library/Application Support/Claude/claude_desktop_config.js } ``` -The API key is held in `env` and referenced via `${API_KEY}` in the `--header` value because `mcp-remote` parses header args before shell expansion — inlining the key after the colon breaks parsing. - -Fully quit and relaunch Claude Desktop (`Cmd+Q` on macOS, **File → Quit** on Windows). The openstatus tools appear in the tool picker after restart. +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 -In Cursor, open **Settings → MCP → Add Server**: +Add the server to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global): ```json { - "openstatus": { - "type": "http", - "url": "https://api.openstatus.dev/mcp", - "headers": { - "x-openstatus-key": "os_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + "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: @@ -195,5 +290,7 @@ Error messages are not redacted — the consumer is an LLM that benefits from th 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 API key's stable identifier (not the workspace id) — trace a specific key with `WHERE actor_id = ''`. -- `actor_user_id` = the openstatus user who created the API key (custom keys only) — attribute mutations back to a person with a join to the `user` table. +- `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_`. Trace a specific credential with `WHERE actor_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`. diff --git a/apps/web/src/content/pages/docs/sdk/nodejs/authentication.mdx b/apps/web/src/content/pages/docs/sdk/nodejs/authentication.mdx index 112e1b6bf..9390cf66c 100644 --- a/apps/web/src/content/pages/docs/sdk/nodejs/authentication.mdx +++ b/apps/web/src/content/pages/docs/sdk/nodejs/authentication.mdx @@ -66,7 +66,7 @@ Scope is set when the key is created and is **immutable** — to change it, revo } ``` -API keys are managed from the dashboard (**Settings → General → API Keys**); there is no public REST or ConnectRPC endpoint for creating or revoking them. Internally, revocation carries one carve-out: a read-only key is allowed to revoke *itself*, so a leaked key always has a rotation path. Revoking a *different* key from a read-only actor is forbidden. +API keys are managed from the dashboard (**Settings > General > API Keys**); there is no public REST or ConnectRPC endpoint for creating or revoking them. Internally, revocation carries one carve-out: a read-only key is allowed to revoke *itself*, so a leaked key always has a rotation path. Revoking a *different* key from a read-only actor is forbidden. ## Custom Base URL diff --git a/apps/web/src/content/pages/docs/tutorial/get-started-with-openstatus-cli.mdx b/apps/web/src/content/pages/docs/tutorial/get-started-with-openstatus-cli.mdx index 01fa38abf..2b88602f9 100644 --- a/apps/web/src/content/pages/docs/tutorial/get-started-with-openstatus-cli.mdx +++ b/apps/web/src/content/pages/docs/tutorial/get-started-with-openstatus-cli.mdx @@ -9,7 +9,7 @@ description: "Step-by-step tutorial to install and use the openstatus CLI for mo |---|---| | **Time** | ~10 minutes | | **Level** | Intermediate | -| **Prerequisites** | openstatus account, command-line experience, API key from your workspace (Settings → General → API Keys) | +| **Prerequisites** | openstatus account, command-line experience, API key from your workspace (Settings > General > API Keys) | In this tutorial, you'll use the openstatus CLI to manage your monitors as code. This lets you version-control your monitoring configuration, automate deployments, and implement GitOps workflows. @@ -64,7 +64,7 @@ openstatus version x.x.x ## Configure API authentication -1. In your openstatus dashboard, go to **Settings → General** and find the **API Keys** card. +1. In your openstatus dashboard, go to **Settings > General** and find the **API Keys** card. 2. Click **Create** and copy the value — you won't see it again after closing the dialog. 3. Make it available to the CLI as an environment variable: @@ -139,7 +139,7 @@ export PATH="$HOME/.local/bin:$PATH" **Fix:** 1. Verify the token is set: `echo $OPENSTATUS_API_TOKEN` -2. Regenerate the key in your workspace settings (Settings → General → API Keys) +2. Regenerate the key in your workspace settings (Settings > General > API Keys) 3. Make sure there are no extra spaces or newlines in the token value ### "no monitors found" on import diff --git a/apps/web/src/content/pages/guides/connect-openstatus-to-chatgpt.mdx b/apps/web/src/content/pages/guides/connect-openstatus-to-chatgpt.mdx new file mode 100644 index 000000000..5373cd7b2 --- /dev/null +++ b/apps/web/src/content/pages/guides/connect-openstatus-to-chatgpt.mdx @@ -0,0 +1,198 @@ +--- +title: "Connect Openstatus to ChatGPT (MCP Setup Guide)" +author: "openstatus" +description: "Add the openstatus MCP server to ChatGPT as a custom connector so the assistant can list status pages, draft incidents, and schedule maintenance from chat — sign in with OAuth, with audit-logged mutations and explicit notify semantics." +publishedAt: "2026-09-08" +updatedAt: "2026-09-08" +category: "education" +howto: + totalTime: "PT3M" + steps: + - name: "Enable developer mode" + text: "In ChatGPT on the web, open Settings > Security and login and switch on Developer mode. Custom MCP connectors with write tools require it." + url: "#step-1-enable-developer-mode" + - name: "Add openstatus as a custom connector" + text: "Open Settings > Connectors > Create, name it openstatus, enter https://api.openstatus.dev/mcp as the MCP server URL, and pick OAuth as the authentication method." + url: "#step-2-add-a-custom-connector" + - name: "Approve the connection" + text: "Sign in to openstatus in the browser, pick the workspace and read-only or read & write access, and approve." + url: "#step-3-approve-the-connection" + - name: "Enable the connector in a chat and run your first prompt" + text: "In a new chat, open the + menu, choose Developer mode, select openstatus, and ask ChatGPT to list your status pages or draft an incident. ChatGPT asks for confirmation before each write tool call." + url: "#step-4-run-your-first-prompt" +faq: + - question: "Does ChatGPT support remote MCP servers like openstatus?" + answer: "Yes. With developer mode enabled, ChatGPT on the web lets you add any remote MCP server as a custom connector. Add openstatus under Settings > Connectors > Create with the URL `https://api.openstatus.dev/mcp` and OAuth authentication; the connection is authorized in your browser. No local process, no config file." + - question: "Which ChatGPT plans can add custom MCP connectors?" + answer: "Developer mode is available on Plus, Pro, Business, Enterprise, and Edu plans on the web. On Business, Enterprise, and Edu, a workspace admin may need to allow custom connectors first. The Free plan cannot add custom connectors." + - question: "Do I need an API key to connect ChatGPT?" + answer: "No. The openstatus MCP server is an OAuth 2.1 authorization server. ChatGPT opens a consent screen where you pick a workspace and an access level; tokens are issued and refreshed automatically. ChatGPT's connector form has no header field, so an API key cannot be used here — use the ChatGPT desktop app or Codex for header-based auth." + - question: "Why don't I see the openstatus tools in a chat?" + answer: "Custom connectors are enabled per chat. Open the + menu, choose Developer mode, and select openstatus. If the connector shows as disconnected in Settings > Connectors, open it and reconnect." + - question: "Can ChatGPT accidentally notify subscribers when posting an incident?" + answer: "No. Every publishing tool (`create_status_report`, `add_status_report_update`, `resolve_status_report`, `create_maintenance`) requires an explicit `notify: true | false` field. ChatGPT must show the notify choice before firing the tool, so an LLM cannot quietly fan out an alert by omitting the flag. ChatGPT also asks you to confirm every write tool call and shows the exact JSON it is about to send. `update_status_report` is metadata-only and has no notify path at all." + - question: "How do I give ChatGPT read-only access to my workspace?" + answer: "Pick Read-only on the consent screen. The MCP server filters write tools out of the `tools/list` response for read-only connections, so ChatGPT sees only the 14 read tools — status pages and page components, status reports, maintenances, monitors, response logs, notifications, private locations, and audit logs. None of the `create_*`, `update_*`, or `resolve_*` tools are registered for that session." + - question: "Where do I revoke ChatGPT's access?" + answer: "In the openstatus dashboard under Settings > Integrations > Connected apps. Revoking signs the connector out immediately; reconnect from ChatGPT's connector settings. Members revoke their own connections, owners and admins can revoke any." + - question: "Where do MCP-driven changes appear in the audit log?" + answer: "Every mutation routed through ChatGPT lands in the audit log under `actor_type = 'mcp'`, with `actor_id` set to the connection (`oat_`) and `actor_user_id` set to the user who approved the connection. This separates AI-driven actions from CLI, dashboard, and direct API mutations." +--- + +**TL;DR** — Switch on developer mode, add `https://api.openstatus.dev/mcp` as a custom connector, approve it in the browser, and ChatGPT can drive your status pages: list incidents, draft updates, and schedule maintenance from the chat window. No config file, no key to paste. Every mutation is audit-logged under `actor_type = 'mcp'` and notifications never fire unless you explicitly approve. + +--- + +[ChatGPT](https://chatgpt.com) speaks the [Model Context Protocol](https://modelcontextprotocol.io) (MCP) — the open standard for letting AI assistants reach external services with explicit approval on every call. This guide wires the [openstatus MCP server](/tooling/mcp-server) into ChatGPT as a custom connector so you can list status pages, post incident updates, resolve reports, and schedule maintenance windows without leaving the chat window. + +## Why connect Openstatus to ChatGPT? + +If your incident workflow already runs through chat, ChatGPT is a natural drafting surface. Concrete scenarios: + +- **Drafting customer-facing copy** — turn engineer shorthand ("payment-api spiking p99 to 6s") into the calm, structured update your subscribers expect, then approve and post it without leaving the chat. +- **On-call summaries** — "Summarize the last 7 days of incidents on my API page" in seconds, useful for weekly reviews and handoffs. +- **Read-only audits** — a teammate connects with read-only access; their ChatGPT sees only the `list_*` and `get_*` tools and cannot mutate anything. + +The same server works with [Codex](/guides/connect-openstatus-to-codex), [Claude Code](/guides/connect-openstatus-to-claude-code), [Claude Desktop](/guides/connect-openstatus-to-claude-desktop), [opencode](/guides/connect-openstatus-to-opencode), Cursor, and any other MCP-compatible client. + +## Prerequisites + +- An [openstatus account](https://www.openstatus.dev) with at least one status page. +- ChatGPT on the web, signed in on a Plus, Pro, Business, Enterprise, or Edu plan. On Business, Enterprise, and Edu, a workspace admin may need to allow custom connectors. + +## Step 1: Enable developer mode + +Custom MCP connectors — and especially their write tools — are gated behind developer mode. Open **Settings > Security and login** and switch on **Developer mode**. This is a one-time toggle per account. + +## Step 2: Add a custom connector + +Open **Settings > Connectors** and click **Create**. Fill in: + +- **Name:** `openstatus` +- **Description:** something short, e.g. `Status pages, incidents, and maintenance windows` +- **MCP server URL:** `https://api.openstatus.dev/mcp` +- **Authentication:** **OAuth** + +Leave the client id and client secret fields empty. openstatus registers the connector automatically; there is nothing to enter. + +## Step 3: Approve the connection + +Click **Create**. ChatGPT opens the openstatus consent screen: + +- **Workspace** — the workspace this connector can reach. If you belong to several, pick one; each workspace is a separate connection. +- **Access** — **Read-only** lets ChatGPT list status pages, reports, monitors, and maintenance windows. **Read & write** also lets it create reports, append updates, resolve incidents, and schedule maintenance. + +Click **Approve**. You land back in ChatGPT with the connector listed and its tools discovered. Tokens refresh in the background; you won't be asked again unless the connection is revoked. + +The connector's detail page in Settings lists every tool the server exposed. With read-only access you'll see `list_status_pages`, `list_status_reports`, `list_maintenances`, `list_monitors`, and the other read tools. With read & write you'll also see `create_status_report`, `add_status_report_update`, `update_status_report`, `resolve_status_report`, and `create_maintenance`. You can switch individual tools off here, and **Refresh** re-syncs the list after a scope change. + +## Step 4: Run your first prompt + +Custom connectors are enabled per chat. Open a new chat, click the **+** button, choose **Developer mode**, and select **openstatus**. + +Try a read tool first: + +``` +List my openstatus status pages +``` + +ChatGPT calls `list_status_pages` and returns the slugs and ids in your workspace. With write access, draft an incident — ChatGPT shows you the title, status, message, and notify choice **before** firing: + +``` +Draft a status report on my "api" page: investigating elevated latency on the payment endpoint, do not notify subscribers yet +``` + +Write tools require confirmation by default. ChatGPT shows the exact JSON it is about to send to `create_status_report`, including `notify: false`; review it, then approve or deny. You can remember the choice for the rest of the conversation. + +## Prefer a desktop or terminal surface? + +ChatGPT's connector form has no header field, so this path is OAuth-only. If you need an API key — a shared machine, or a key that outlives your user — use the **ChatGPT desktop app** or **Codex** instead. Both read the same `~/.codex/config.toml`: in the desktop app open **Settings > MCP servers > Add server**, choose **Streamable HTTP**, enter the URL, save, and choose **Restart**. The [Codex guide](/guides/connect-openstatus-to-codex) covers the header-based setup. + +## Built-in safeguards + +Four properties make this safe enough for a real incident: + +- **Workspace-scoped consent** — the token only reaches the workspace you approved, with the access level you chose. A connector that asks for write access can be downgraded to read-only on the consent screen. +- **Required `notify: boolean`** on every mutation tool — the schema rejects calls that omit it, so the LLM has to ask before firing. `notify: false` is a hard guarantee that no subscriber alert goes out. +- **Lookup-before-mutate** — `create_status_report` and `create_maintenance` require a `pageId` resolved via `list_status_pages` rather than guessed. +- **Audit log attribution** — every mutation lands in the audit log under `actor_type = 'mcp'`, with `actor_id` (the connection) and `actor_user_id` (the user who approved it), so AI-driven actions are traceable as a separate slice. Connecting and revoking are audited too. + +## Disconnecting + +Open **Settings > Integrations > Connected apps** in the openstatus dashboard. Each connection shows the client, its access, who connected it, and when it was last used. Revoke it and the connector's next call fails; reconnect from ChatGPT's **Settings > Connectors**. Members revoke their own connections; owners and admins can revoke any. Removing a member from the workspace revokes their connections automatically. + +To remove the connector on the ChatGPT side, open it under **Settings > Connectors** and delete it. + +## Troubleshooting + +- **There is no Create button under Connectors.** Developer mode is off, or your plan or workspace doesn't allow custom connectors. Check **Settings > Security and login > Developer mode**, then ask your workspace admin if the button is still missing. +- **The connector shows as disconnected.** Open **Settings > Connectors**, click openstatus, and reconnect. This is expected after a revoke from the dashboard. +- **Tools don't show up in a chat.** Connectors are enabled per chat: click **+**, choose **Developer mode**, and select openstatus. +- **"Tool not available" when creating an incident.** Your connection is read-only. Reconnect and pick **Read & write** on the consent screen, then **Refresh** the tool list on the connector's detail page. +- **ChatGPT reports that the server URL failed to load.** Sanity-check reachability with `curl -i -H "Accept: text/event-stream" https://api.openstatus.dev/mcp` — a `401` with a `WWW-Authenticate` header means the server is up and waiting for a sign-in. + +## What's next? + +- **[MCP Server Reference](/docs/reference/mcp-server)** — full tool schema, error codes, OAuth endpoints, and scope mechanics. +- **[Connect to Codex](/guides/connect-openstatus-to-codex)** — terminal-shaped variant of this guide. +- **[Connect to Claude Desktop](/guides/connect-openstatus-to-claude-desktop)** — the same setup for Anthropic's chat client. +- **[Openstatus CLI](/tooling/cli)** — for terminal workflows that don't need an LLM in the loop. +- **[Slack Agent](/status-page#slack-agent)** — for teams that live in Slack. + +## Frequently asked questions + +
    + +Yes. With developer mode enabled, ChatGPT on the web lets you add any remote MCP server as a custom connector. Add openstatus under **Settings > Connectors > Create** with the URL `https://api.openstatus.dev/mcp` and **OAuth** authentication; the connection is authorized in your browser. No local process, no config file. + +
    + +
    + +Developer mode is available on Plus, Pro, Business, Enterprise, and Edu plans on the web. On Business, Enterprise, and Edu, a workspace admin may need to allow custom connectors first. The Free plan cannot add custom connectors. + +
    + +
    + +No. The openstatus MCP server is an OAuth 2.1 authorization server. ChatGPT opens a consent screen where you pick a workspace and an access level; tokens are issued and refreshed automatically. ChatGPT's connector form has no header field, so an API key cannot be used here — use the ChatGPT desktop app or [Codex](/guides/connect-openstatus-to-codex) for header-based auth. + +
    + +
    + +Custom connectors are enabled per chat. Open the **+** menu, choose **Developer mode**, and select openstatus. If the connector shows as disconnected in **Settings > Connectors**, open it and reconnect. + +
    + +
    + +No. Every publishing tool (`create_status_report`, `add_status_report_update`, `resolve_status_report`, `create_maintenance`) requires an explicit `notify: true | false` field. ChatGPT must show the notify choice before firing the tool, so an LLM cannot quietly fan out an alert by omitting the flag. ChatGPT also asks you to confirm every write tool call and shows the exact JSON it is about to send. `update_status_report` is metadata-only and has no notify path at all. + +
    + +
    + +Pick **Read-only** on the consent screen. The MCP server filters write tools out of the `tools/list` response for read-only connections, so ChatGPT sees only the 14 read tools — status pages and page components, status reports, maintenances, monitors, response logs, notifications, private locations, and audit logs. None of the `create_*`, `update_*`, or `resolve_*` tools are registered for that session. + +
    + +
    + +In the openstatus dashboard under **Settings > Integrations > Connected apps**. Revoking signs the connector out immediately; reconnect from ChatGPT's connector settings. Members revoke their own connections, owners and admins can revoke any. + +
    + +
    + +Every mutation routed through ChatGPT lands in the audit log under `actor_type = 'mcp'`, with `actor_id` set to the connection (`oat_`) and `actor_user_id` set to the user who approved the connection. This separates AI-driven actions from CLI, dashboard, and direct API mutations. + +
    + +--- + +Start free. No credit card required. Configure ChatGPT to drive your status pages in under 5 minutes. + +Try openstatus free + +--- diff --git a/apps/web/src/content/pages/guides/connect-openstatus-to-claude-code.mdx b/apps/web/src/content/pages/guides/connect-openstatus-to-claude-code.mdx index 5ad837c2b..6d5400c6c 100644 --- a/apps/web/src/content/pages/guides/connect-openstatus-to-claude-code.mdx +++ b/apps/web/src/content/pages/guides/connect-openstatus-to-claude-code.mdx @@ -1,20 +1,21 @@ --- title: "Connect Openstatus to Claude Code (MCP Setup Guide)" author: "openstatus" -description: "Wire the openstatus MCP server into Claude Code so the agent can list status pages, draft incidents, and schedule maintenance from your terminal — with audit-logged mutations and explicit notify semantics." +description: "Wire the openstatus MCP server into Claude Code so the agent can list status pages, draft incidents, and schedule maintenance from your terminal — sign in with OAuth, no key to paste, with audit-logged mutations and explicit notify semantics." publishedAt: "2026-05-08" +updatedAt: "2026-09-07" category: "education" howto: - totalTime: "PT3M" + totalTime: "PT2M" steps: - - name: "Create an openstatus API key" - text: "Open Settings → API Tokens in the openstatus dashboard, click Create Token, and pick read-only or read & write scope." - url: "#step-1-create-an-api-key" - name: "Register the MCP server with Claude Code" - text: "Run `claude mcp add --transport http --scope user --header \"x-openstatus-key: os_…\" openstatus https://api.openstatus.dev/mcp` to attach the openstatus tools to every Claude Code session." - url: "#step-2-register-the-mcp-server" + text: "Run `claude mcp add --transport http --scope user openstatus https://api.openstatus.dev/mcp` to attach the openstatus tools to every Claude Code session." + url: "#step-1-register-the-mcp-server" + - name: "Sign in with OAuth" + text: "In Claude Code, run /mcp, select openstatus, and choose Authenticate. Approve the connection in the browser, picking a workspace and read-only or read & write access." + url: "#step-2-sign-in" - name: "Verify the connection" - text: "In Claude Code, run /mcp and confirm openstatus is listed as connected with the expected tools (list_status_pages, list_status_reports, …)." + text: "Run /mcp again and confirm openstatus is listed as connected with the expected tools (list_status_pages, list_status_reports, …)." url: "#step-3-verify-the-connection" - name: "Run your first prompt" text: "Ask Claude Code to list your status pages or draft an incident. The agent will request approval before each tool call." @@ -23,24 +24,26 @@ faq: - question: "Does Claude Code support remote MCP servers like openstatus?" answer: "Yes. Claude Code supports stdio, HTTP, and SSE transports. The openstatus MCP server is a stateless Streamable HTTP endpoint, registered with `claude mcp add --transport http`." - question: "What can Claude Code do once the openstatus MCP server is connected?" - answer: "Claude Code can list status pages, status reports, and maintenance windows; create new status reports; append updates to existing reports; resolve reports; edit metadata; and schedule maintenance windows — all scoped to the workspace tied to your API key." - - question: "Do I need a separate MCP credential for Claude Code?" - answer: "No. Authentication is the standard `x-openstatus-key` header — the same API key the openstatus CLI, REST API, and Terraform provider use. There is no MCP-specific OAuth flow." + answer: "Claude Code can list status pages, status reports, and maintenance windows; create new status reports; append updates to existing reports; resolve reports; edit metadata; and schedule maintenance windows — all scoped to the workspace you picked when you approved the connection." + - question: "Do I need an API key to connect Claude Code?" + answer: "No. The openstatus MCP server is an OAuth 2.1 authorization server. Claude Code opens a consent screen in your browser where you pick a workspace and an access level; tokens are issued and refreshed automatically. An API key in the `x-openstatus-key` header still works for CI and headless agents." - question: "Can Claude Code accidentally notify subscribers when creating an incident?" answer: "No. Every publishing tool (`create_status_report`, `add_status_report_update`, `resolve_status_report`, `create_maintenance`) requires an explicit `notify: true | false` field. Claude Code must show the notify choice before firing the tool, so an LLM cannot quietly fan out an alert by omitting the flag. `update_status_report` is metadata-only and has no notify path at all." - question: "How do I give Claude Code read-only access to my workspace?" - answer: "Create an API key with `read` scope. The MCP server filters write tools out of the `tools/list` response for read-only keys, so Claude Code sees only the 14 read tools — status pages and page components, status reports, maintenances, monitors, response logs, notifications, private locations, and audit logs. None of the `create_*`, `update_*`, or `resolve_*` tools are registered for that session." + answer: "Pick Read-only on the consent screen. The MCP server filters write tools out of the `tools/list` response for read-only connections, so Claude Code sees only the 14 read tools — status pages and page components, status reports, maintenances, monitors, response logs, notifications, private locations, and audit logs. None of the `create_*`, `update_*`, or `resolve_*` tools are registered for that session." + - question: "How do I disconnect Claude Code from my workspace?" + answer: "Open Settings > Integrations > Connected apps in the openstatus dashboard and revoke the connection. Claude Code's next call fails with 401; run /mcp > Authenticate to reconnect. Members revoke their own connections, owners and admins can revoke any." - question: "Where do MCP-driven changes appear in the audit log?" - answer: "Every mutation routed through the MCP server lands in the audit log under `actor_type = 'mcp'`, with `actor_id` set to the API key id and `actor_user_id` set to the user who created the key. This separates AI-driven actions from CLI, dashboard, and direct API mutations." + answer: "Every mutation routed through the MCP server lands in the audit log under `actor_type = 'mcp'`, with `actor_id` set to the connection (`oat_` for OAuth, the key id for an API key) and `actor_user_id` set to the user who approved the connection or created the key. This separates AI-driven actions from CLI, dashboard, and direct API mutations." - question: "Can I commit the MCP configuration to my repository?" - answer: "Yes. Create a project-scoped `.mcp.json` at the repo root with the MCP server definition, and use `${OPENSTATUS_API_KEY}` for the header value so the secret stays out of source control." + answer: "Yes. Create a project-scoped `.mcp.json` at the repo root with the MCP server definition and no header; each teammate signs in with OAuth on first use. For CI, add an `x-openstatus-key` header with `${OPENSTATUS_API_KEY}` so the secret stays out of source control." --- -**TL;DR** — Run one `claude mcp add` command with your openstatus API key and Claude Code can drive your status pages: list incidents, draft updates, and schedule maintenance from your terminal. Every mutation is audit-logged under `actor_type = 'mcp'` and notifications never fire unless you explicitly approve. +**TL;DR** — Run one `claude mcp add` command, sign in from `/mcp`, and Claude Code can drive your status pages: list incidents, draft updates, and schedule maintenance from your terminal. No key to paste. Every mutation is audit-logged under `actor_type = 'mcp'` and notifications never fire unless you explicitly approve. --- -[Claude Code](https://docs.claude.com/en/docs/claude-code/overview) is an agent that lives in your terminal. The [Model Context Protocol](https://modelcontextprotocol.io) (MCP) lets it reach out to remote services like openstatus — with the agent's host responsible for approval and audit. This guide walks through the three-minute setup. +[Claude Code](https://docs.claude.com/en/docs/claude-code/overview) is an agent that lives in your terminal. The [Model Context Protocol](https://modelcontextprotocol.io) (MCP) lets it reach out to remote services like openstatus — with the agent's host responsible for approval and audit. This guide walks through the two-minute setup. ## Why connect Openstatus to Claude Code? @@ -48,25 +51,16 @@ Status page work usually lands while you're already debugging in a terminal. Con - **During an incident** — draft an investigating-status update for the right page and approve it without opening the dashboard. - **At deploy time** — schedule a maintenance window from the same Claude Code session you used to ship the change. -- **Read-only audits** — hand a teammate a read-scoped key so their Claude Code can summarize page health with zero mutation risk. +- **Read-only audits** — a teammate connects with read-only access; their Claude Code can summarize page health with zero mutation risk. -The same key works with [Claude Desktop](/guides/connect-openstatus-to-claude-desktop), Cursor, ChatGPT, and any other MCP-compatible client. +The same server works with [Claude Desktop](/guides/connect-openstatus-to-claude-desktop), Claude.ai, [Codex](/guides/connect-openstatus-to-codex), [ChatGPT](/guides/connect-openstatus-to-chatgpt), [opencode](/guides/connect-openstatus-to-opencode), Cursor, and any other MCP-compatible client. ## Prerequisites - An [openstatus account](https://www.openstatus.dev) with at least one status page. - Claude Code installed (`claude --version` should print a version). -## Step 1: Create an API key - -In the openstatus dashboard, go to **Settings → API Tokens** → **Create Token**. Pick a scope: - -- **Read-only** — Claude Code can only list status pages, reports, and maintenance windows. -- **Read & write** — Claude Code can also create reports, append updates, resolve incidents, and schedule maintenance. - -Scopes are immutable on a key. Copy the value (starts with `os_`); you'll only see it once. - -## Step 2: Register the MCP server +## Step 1: Register the MCP server Claude Code talks to the openstatus MCP server over Streamable HTTP. Register it with `claude mcp add`: @@ -74,7 +68,6 @@ Claude Code talks to the openstatus MCP server over Streamable HTTP. Register it claude mcp add \ --transport http \ --scope user \ - --header "x-openstatus-key: os_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ openstatus \ https://api.openstatus.dev/mcp ``` @@ -91,18 +84,15 @@ To commit the configuration to a repo so your team picks it up automatically, cr "mcpServers": { "openstatus": { "type": "http", - "url": "https://api.openstatus.dev/mcp", - "headers": { - "x-openstatus-key": "${OPENSTATUS_API_KEY}" - } + "url": "https://api.openstatus.dev/mcp" } } } ``` -Export `OPENSTATUS_API_KEY` in your shell — Claude Code expands it at connection time, so the key never lands in source control. +No secret in the file: each teammate signs in with OAuth the first time they use it. -## Step 3: Verify the connection +## Step 2: Sign in In Claude Code, run: @@ -110,7 +100,16 @@ In Claude Code, run: /mcp ``` -`openstatus` should be listed as **connected**. With a read-only key you'll see `list_status_pages`, `list_status_reports`, `list_maintenances`. With a read & write key you'll also see `create_status_report`, `add_status_report_update`, `update_status_report`, `resolve_status_report`, and `create_maintenance`. The server filters mutation tools out for read-only keys, so a misconfigured client cannot call them. +Select **openstatus** and choose **Authenticate**. Your browser opens the openstatus consent screen: + +- **Workspace** — the workspace this connection can reach. If you belong to several, pick one. To switch later, revoke the connection in **Settings > Integrations > Connected apps** and run **Authenticate** again — while a connection is live you are not shown the consent screen. +- **Access** — **Read-only** lets Claude Code list status pages, reports, and maintenance windows. **Read & write** also lets it create reports, append updates, resolve incidents, and schedule maintenance. + +Click **Approve**. Claude Code receives a token and refreshes it in the background; you won't be asked again unless the connection is revoked. + +## Step 3: Verify the connection + +Run `/mcp` again. `openstatus` should be listed as **connected**. With read-only access you'll see `list_status_pages`, `list_status_reports`, `list_maintenances`. With read & write you'll also see `create_status_report`, `add_status_report_update`, `update_status_report`, `resolve_status_report`, and `create_maintenance`. The server filters mutation tools out for read-only connections, so a misconfigured client cannot call them. ## Step 4: Run your first prompt @@ -122,7 +121,7 @@ Try a read tool first: Claude Code calls `list_status_pages`, asks for your approval, and returns the slugs and ids in your workspace. -With a write-scoped key, draft an incident — Claude Code shows you the title, status, message, and notify choice **before** firing: +With write access, draft an incident — Claude Code shows you the title, status, message, and notify choice **before** firing: ``` > draft a status report on my "api" page: investigating elevated latency on the payment endpoint, do not notify subscribers yet @@ -130,23 +129,46 @@ With a write-scoped key, draft an incident — Claude Code shows you the title, The agent resolves the page id via `list_status_pages`, presents the draft, and only calls `create_status_report` once you confirm. +## Headless setups: use an API key + +OAuth needs a browser on the same machine. In CI, on a remote box, or inside a container, pass an API key instead. Create one in **Settings > API Tokens** (scopes are immutable on a key) and add the header: + +```bash +claude mcp add \ + --transport http \ + --scope user \ + --header "x-openstatus-key: os_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ + openstatus \ + https://api.openstatus.dev/mcp +``` + +In a committed `.mcp.json`, use `"headers": { "x-openstatus-key": "${OPENSTATUS_API_KEY}" }` and export the variable in your shell — Claude Code expands it at connection time. When a header is present, the OAuth sign-in is skipped. + ## Built-in safeguards -Three properties make this safe enough for a real incident: +Four properties make this safe enough for a real incident: +- **Workspace-scoped consent** — the token only reaches the workspace you approved, with the access level you chose. You can downgrade a client that asks for write access to read-only on the consent screen. - **Required `notify: boolean`** on every mutation tool — the schema rejects calls that omit it, so the LLM has to ask before firing. `notify: false` is a hard guarantee that no subscriber alert goes out. - **Lookup-before-mutate** — `create_status_report` and `create_maintenance` require a `pageId` resolved via `list_status_pages` rather than guessed. -- **Audit log attribution** — every mutation lands in the audit log under `actor_type = 'mcp'`, with `actor_id` (key) and `actor_user_id` (the user who created the key), so AI-driven actions are traceable as a separate slice. +- **Audit log attribution** — every mutation lands in the audit log under `actor_type = 'mcp'`, with `actor_id` (the connection) and `actor_user_id` (the user who approved it), so AI-driven actions are traceable as a separate slice. Connecting and revoking are audited too. + +## Disconnecting + +Open **Settings > Integrations > Connected apps** in the dashboard. Each connection shows the client, its access, who connected it, and when it was last used. Revoke it and Claude Code's next call fails with `401`; run `/mcp` > **Authenticate** to reconnect. Members revoke their own connections; owners and admins can revoke any. Removing a member from the workspace revokes their connections automatically. ## Troubleshooting -- **`/mcp` shows openstatus as disconnected.** Run `claude mcp list` and verify the `x-openstatus-key` value matches a key in **Settings → API Tokens** (whitespace from copy-paste is the usual culprit). Sanity-check with `curl -i -H "x-openstatus-key: os_..." https://api.openstatus.dev/mcp` — a `200` plus `mcp-session-id` header means the key is good. -- **"Tool not available" when creating an incident.** Your key is read-only. Issue a new **Read & write** key and re-run `claude mcp add`. +- **`/mcp` shows openstatus as disconnected.** Select it and choose **Authenticate** again. Sanity-check reachability with `curl -i -H "Accept: text/event-stream" https://api.openstatus.dev/mcp` — a `401` with a `WWW-Authenticate` header means the server is up and waiting for a sign-in. +- **The browser never opens.** Claude Code prints the authorization URL; open it in a browser on the same machine, since the callback lands on a `localhost` port Claude Code listens on. For remote sessions, use an [API key](#headless-setups-use-an-api-key). +- **"Tool not available" when creating an incident.** Your connection is read-only. Sign in again and pick **Read & write**. ## What's next? -- **[MCP Server Reference](https://www.openstatus.dev/docs/reference/mcp-server)** — full tool schema, error codes, and scope mechanics. +- **[MCP Server Reference](/docs/reference/mcp-server)** — full tool schema, error codes, OAuth endpoints, and scope mechanics. - **[Connect to Claude Desktop](/guides/connect-openstatus-to-claude-desktop)** — chat-shaped variant of this guide. +- **[Connect to Codex](/guides/connect-openstatus-to-codex)** — the same setup for OpenAI's terminal agent. +- **[Connect to opencode](/guides/connect-openstatus-to-opencode)** — the same setup for the open-source terminal agent. - **[Openstatus CLI](/tooling/cli)** — for terminal workflows that don't need an LLM in the loop. - **[Slack Agent](/status-page#slack-agent)** — for teams that live in Slack. @@ -160,13 +182,13 @@ Yes. Claude Code supports stdio, HTTP, and SSE transports. The openstatus MCP se
    -Claude Code can list status pages, status reports, and maintenance windows; create new status reports; append updates to existing reports; resolve reports; edit metadata; and schedule maintenance windows — all scoped to the workspace tied to your API key. +Claude Code can list status pages, status reports, and maintenance windows; create new status reports; append updates to existing reports; resolve reports; edit metadata; and schedule maintenance windows — all scoped to the workspace you picked when you approved the connection.
    -
    +
    -No. Authentication is the standard `x-openstatus-key` header — the same API key the openstatus CLI, REST API, and Terraform provider use. There is no MCP-specific OAuth flow. +No. The openstatus MCP server is an OAuth 2.1 authorization server. Claude Code opens a consent screen in your browser where you pick a workspace and an access level; tokens are issued and refreshed automatically. An API key in the `x-openstatus-key` header still works for CI and headless agents.
    @@ -178,19 +200,25 @@ No. Every publishing tool (`create_status_report`, `add_status_report_update`, `
    -Create an API key with `read` scope. The MCP server filters write tools out of the `tools/list` response for read-only keys, so Claude Code sees only the 14 read tools — status pages and page components, status reports, maintenances, monitors, response logs, notifications, private locations, and audit logs. None of the `create_*`, `update_*`, or `resolve_*` tools are registered for that session. +Pick **Read-only** on the consent screen. The MCP server filters write tools out of the `tools/list` response for read-only connections, so Claude Code sees only the 14 read tools — status pages and page components, status reports, maintenances, monitors, response logs, notifications, private locations, and audit logs. None of the `create_*`, `update_*`, or `resolve_*` tools are registered for that session. + +
    + +
    + +Open **Settings > Integrations > Connected apps** in the openstatus dashboard and revoke the connection. Claude Code's next call fails with `401`; run `/mcp` > **Authenticate** to reconnect. Members revoke their own connections, owners and admins can revoke any.
    -Every mutation routed through the MCP server lands in the audit log under `actor_type = 'mcp'`, with `actor_id` set to the API key id and `actor_user_id` set to the user who created the key. This separates AI-driven actions from CLI, dashboard, and direct API mutations. +Every mutation routed through the MCP server lands in the audit log under `actor_type = 'mcp'`, with `actor_id` set to the connection (`oat_` for OAuth, the key id for an API key) and `actor_user_id` set to the user who approved the connection or created the key. This separates AI-driven actions from CLI, dashboard, and direct API mutations.
    -Yes. Create a project-scoped `.mcp.json` at the repo root with the MCP server definition, and use `${OPENSTATUS_API_KEY}` for the header value so the secret stays out of source control. +Yes. Create a project-scoped `.mcp.json` at the repo root with the MCP server definition and no header; each teammate signs in with OAuth on first use. For CI, add an `x-openstatus-key` header with `${OPENSTATUS_API_KEY}` so the secret stays out of source control.
    diff --git a/apps/web/src/content/pages/guides/connect-openstatus-to-claude-desktop.mdx b/apps/web/src/content/pages/guides/connect-openstatus-to-claude-desktop.mdx index a1b5dc0d2..c10e84c9b 100644 --- a/apps/web/src/content/pages/guides/connect-openstatus-to-claude-desktop.mdx +++ b/apps/web/src/content/pages/guides/connect-openstatus-to-claude-desktop.mdx @@ -1,46 +1,47 @@ --- title: "Connect Openstatus to Claude Desktop (MCP Setup Guide)" author: "openstatus" -description: "Add the openstatus MCP server to Claude Desktop so the assistant can list status pages, draft incidents, and schedule maintenance from chat — with audit-logged mutations and explicit notify semantics." +description: "Add the openstatus MCP server to Claude Desktop as a custom connector so the assistant can list status pages, draft incidents, and schedule maintenance from chat — sign in with OAuth, with audit-logged mutations and explicit notify semantics." publishedAt: "2026-05-08" +updatedAt: "2026-09-07" category: "education" howto: - totalTime: "PT3M" + totalTime: "PT2M" steps: - - name: "Create an openstatus API key" - text: "Open Settings → API Tokens in the openstatus dashboard, click Create Token, and pick read-only or read & write scope." - url: "#step-1-create-an-api-key" - - name: "Edit claude_desktop_config.json" - text: "Add the openstatus MCP server under `mcpServers` in `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\\Claude\\claude_desktop_config.json` (Windows). Claude Desktop launches MCP servers as local processes, so the remote HTTP server is bridged through `npx mcp-remote`." - url: "#step-2-edit-claude-desktop-config" - - name: "Restart Claude Desktop" - text: "Quit Claude Desktop fully and relaunch it so the new MCP server is registered. Reloading the window is not enough." - url: "#step-3-restart-claude-desktop" - - name: "Verify and run your first prompt" - text: "Open the tool picker, confirm the openstatus tools are listed, then ask Claude to list your status pages or draft an incident." - url: "#step-4-verify-and-run-your-first-prompt" + - name: "Add openstatus as a custom connector" + text: "In Claude Desktop, open Settings > Connectors > Add custom connector, name it openstatus, and enter https://api.openstatus.dev/mcp as the URL." + url: "#step-1-add-a-custom-connector" + - name: "Approve the connection" + text: "Click Connect. Sign in to openstatus in the browser, pick the workspace and read-only or read & write access, and approve." + url: "#step-2-approve-the-connection" + - name: "Enable the tools in a chat" + text: "Open a new chat, open the tool picker, and switch on the openstatus connector." + url: "#step-3-enable-the-tools-in-a-chat" + - name: "Run your first prompt" + text: "Ask Claude to list your status pages or draft an incident. Claude asks for approval before each tool call." + url: "#step-4-run-your-first-prompt" faq: - question: "Does Claude Desktop support remote MCP servers like openstatus?" - answer: "Yes, but indirectly. Claude Desktop launches MCP servers as local stdio processes, so a remote HTTP server like openstatus has to be bridged through `npx mcp-remote` — a small open-source proxy that translates Claude Desktop's stdio calls into HTTP requests. You configure `mcp-remote` once in `claude_desktop_config.json` and Claude Desktop treats openstatus like any other local server." - - question: "Where is claude_desktop_config.json on macOS and Windows?" - answer: "On macOS, the file lives at `~/Library/Application Support/Claude/claude_desktop_config.json`. On Windows, it lives at `%APPDATA%\\Claude\\claude_desktop_config.json`. Create the file if it does not exist." - - question: "Do I need a separate MCP credential for Claude Desktop?" - answer: "No. Authentication is the standard `x-openstatus-key` header — the same API key the openstatus CLI, REST API, and Terraform provider use. There is no MCP-specific OAuth flow." - - question: "Why don't I see the openstatus tools after editing the config?" - answer: "The two most common causes are not fully quitting Claude Desktop (it only loads MCP servers at launch — close the window is not enough, you have to Cmd+Q on macOS or File → Quit on Windows) and `npx` not being on the PATH that Claude Desktop sees. Install Node.js 18+ from nodejs.org if `node --version` is missing in your shell." + answer: "Yes. Claude Desktop and Claude.ai share a connector list. Add openstatus under Settings > Connectors > Add custom connector with the URL `https://api.openstatus.dev/mcp`; the connection is authorized with OAuth in your browser. No local process, no Node.js, no config file." + - question: "Do I need an API key to connect Claude Desktop?" + answer: "No. The openstatus MCP server is an OAuth 2.1 authorization server. Claude Desktop opens a consent screen where you pick a workspace and an access level; tokens are issued and refreshed automatically. If you prefer an API key, you can still bridge the server through `npx mcp-remote` with an `x-openstatus-key` header in `claude_desktop_config.json`." + - question: "Where do I revoke Claude Desktop's access?" + answer: "In the openstatus dashboard under Settings > Integrations > Connected apps. Revoking signs the connector out immediately; reconnect from Claude Desktop's connector settings. Members revoke their own connections, owners and admins can revoke any." + - question: "Why don't I see the openstatus tools in a chat?" + answer: "Connectors are enabled per chat. Open the tool picker in the chat and switch the openstatus connector on. If the connector shows as disconnected in Settings > Connectors, click it and reconnect." - question: "Can Claude Desktop accidentally notify subscribers when posting an incident?" answer: "No. Every publishing tool (`create_status_report`, `add_status_report_update`, `resolve_status_report`, `create_maintenance`) requires an explicit `notify: true | false` field. Claude must show the notify choice before firing the tool, so an LLM cannot quietly fan out an alert by omitting the flag. `update_status_report` is metadata-only and has no notify path at all." - question: "How do I give Claude Desktop read-only access to my workspace?" - answer: "Create an API key with `read` scope. The MCP server filters write tools out of the `tools/list` response for read-only keys, so Claude Desktop sees only the 14 read tools — status pages and page components, status reports, maintenances, monitors, response logs, notifications, private locations, and audit logs. None of the `create_*`, `update_*`, or `resolve_*` tools are registered for that session." + answer: "Pick Read-only on the consent screen. The MCP server filters write tools out of the `tools/list` response for read-only connections, so Claude Desktop sees only the 14 read tools — status pages and page components, status reports, maintenances, monitors, response logs, notifications, private locations, and audit logs. None of the `create_*`, `update_*`, or `resolve_*` tools are registered for that session." - question: "Where do MCP-driven changes appear in the audit log?" - answer: "Every mutation routed through Claude Desktop lands in the audit log under `actor_type = 'mcp'`, with `actor_id` set to the API key id and `actor_user_id` set to the user who created the key. This separates AI-driven actions from CLI, dashboard, and direct API mutations." + answer: "Every mutation routed through Claude Desktop lands in the audit log under `actor_type = 'mcp'`, with `actor_id` set to the connection (`oat_` for OAuth, the key id for an API key) and `actor_user_id` set to the user who approved the connection or created the key. This separates AI-driven actions from CLI, dashboard, and direct API mutations." --- -**TL;DR** — Drop a small `mcp-remote` block into `claude_desktop_config.json`, restart Claude Desktop, and the assistant can drive your status pages: list incidents, draft updates, and schedule maintenance from the chat window. Every mutation is audit-logged under `actor_type = 'mcp'` and notifications never fire unless you explicitly approve. +**TL;DR** — Add `https://api.openstatus.dev/mcp` as a custom connector, approve it in the browser, and Claude Desktop can drive your status pages: list incidents, draft updates, and schedule maintenance from the chat window. No config file, no key to paste. Every mutation is audit-logged under `actor_type = 'mcp'` and notifications never fire unless you explicitly approve. --- -[Claude Desktop](https://claude.com/download) speaks the [Model Context Protocol](https://modelcontextprotocol.io) (MCP) — the open standard for letting AI assistants reach external services with explicit approval on every call. This guide wires the [openstatus MCP server](/tooling/mcp-server) into Claude Desktop so you can list status pages, post incident updates, resolve reports, and schedule maintenance windows without leaving the chat window. +[Claude Desktop](https://claude.com/download) speaks the [Model Context Protocol](https://modelcontextprotocol.io) (MCP) — the open standard for letting AI assistants reach external services with explicit approval on every call. This guide wires the [openstatus MCP server](/tooling/mcp-server) into Claude Desktop so you can list status pages, post incident updates, resolve reports, and schedule maintenance windows without leaving the chat window. The same steps work in Claude.ai in the browser: the two share one connector list. ## Why connect Openstatus to Claude Desktop? @@ -48,35 +49,54 @@ If your incident workflow already runs through chat, Claude Desktop is the right - **Drafting customer-facing copy** — turn engineer shorthand ("payment-api spiking p99 to 6s") into the calm, structured update your subscribers expect, then approve and post it without leaving the chat. - **On-call summaries** — "Summarize the last 7 days of incidents on my API page" in seconds, useful for weekly reviews and handoffs. -- **Read-only audits** — issue a read-scoped key to a teammate; their Claude Desktop sees only the `list_*` tools and cannot mutate anything. +- **Read-only audits** — a teammate connects with read-only access; their Claude Desktop sees only the `list_*` and `get_*` tools and cannot mutate anything. -The same key works with [Claude Code](/guides/connect-openstatus-to-claude-code), Cursor, ChatGPT, and any other MCP-compatible client. +The same server works with [Claude Code](/guides/connect-openstatus-to-claude-code), [ChatGPT](/guides/connect-openstatus-to-chatgpt), [Codex](/guides/connect-openstatus-to-codex), [opencode](/guides/connect-openstatus-to-opencode), Cursor, and any other MCP-compatible client. ## Prerequisites - An [openstatus account](https://www.openstatus.dev) with at least one status page. -- [Claude Desktop](https://claude.com/download) installed and signed in. -- **Node.js 18+** on your PATH — Claude Desktop uses `npx` to launch the `mcp-remote` bridge. Confirm with `node --version`; install from [nodejs.org](https://nodejs.org) if missing. +- [Claude Desktop](https://claude.com/download) installed and signed in, on a plan that allows custom connectors. -## Step 1: Create an API key +## Step 1: Add a custom connector -In the openstatus dashboard, go to **Settings → API Tokens** → **Create Token**. Pick a scope: +In Claude Desktop, open **Settings > Connectors** and click **Add custom connector**. Fill in: -- **Read-only** — Claude Desktop can only list status pages, reports, and maintenance windows. -- **Read & write** — Claude Desktop can also create reports, append updates, resolve incidents, and schedule maintenance. +- **Name:** `openstatus` +- **Remote MCP server URL:** `https://api.openstatus.dev/mcp` -Scopes are immutable on a key. Copy the value (starts with `os_`); you'll only see it once. +Leave the advanced OAuth fields empty. openstatus registers the connector automatically; there is no client id or secret to enter. -## Step 2: Edit `claude_desktop_config.json` +## Step 2: Approve the connection -Claude Desktop reads its MCP servers from a single JSON file: +Click **Connect**. Your browser opens the openstatus consent screen: -- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` -- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` +- **Workspace** — the workspace this connector can reach. If you belong to several, pick one; each workspace is a separate connection. +- **Access** — **Read-only** lets Claude list status pages, reports, monitors, and maintenance windows. **Read & write** also lets it create reports, append updates, resolve incidents, and schedule maintenance. -The fastest way to open it is from inside Claude Desktop: **Settings → Developer → Edit Config**. Create the file if it doesn't exist. +Click **Approve**. You land back in Claude Desktop with the connector marked as connected. Tokens refresh in the background; you won't be asked again unless the connection is revoked. -Add the openstatus server under `mcpServers`: +## Step 3: Enable the tools in a chat + +Open a new chat and click the tool picker. Switch the **openstatus** connector on. With read-only access you'll see `list_status_pages`, `list_status_reports`, `list_maintenances`. With read & write you'll also see `create_status_report`, `add_status_report_update`, `update_status_report`, `resolve_status_report`, and `create_maintenance`. + +## Step 4: Run your first prompt + +Try a read tool first: + +``` +List my openstatus status pages +``` + +Claude calls `list_status_pages`, asks for approval, and returns the slugs and ids in your workspace. With write access, draft an incident — Claude shows you the title, status, message, and notify choice **before** firing: + +``` +Draft a status report on my "api" page: investigating elevated latency on the payment endpoint, do not notify subscribers yet +``` + +## Alternative: API key with `mcp-remote` + +If you'd rather authenticate with an API key — a shared machine, or a key that outlives your user — Claude Desktop can bridge the server through [`mcp-remote`](https://www.npmjs.com/package/mcp-remote). You'll need Node.js 18+ on your PATH. Create a key in **Settings > API Tokens** (scopes are immutable on a key), then add the server to `claude_desktop_config.json` (**Settings > Developer > Edit Config**): ```json { @@ -97,48 +117,33 @@ Add the openstatus server under `mcpServers`: } ``` -Two things worth knowing: - -- **Claude Desktop launches MCP servers as local processes**, so it can't talk to a remote HTTP endpoint directly. [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) is an open-source proxy that exposes a local stdio MCP server and forwards calls to the remote URL. `npx` runs it on demand — no global install. -- **The API key lives in `env`, not inlined in `args`.** `mcp-remote` parses `--header` arguments before shell expansion, so a literal key after the colon breaks parsing. Holding it in `env.API_KEY` and referencing `${API_KEY}` in the header value is the documented workaround. - -## Step 3: Restart Claude Desktop - -Claude Desktop only loads MCP servers at launch. Quit fully (`Cmd+Q` on macOS, **File → Quit** on Windows) and reopen — reloading the window is not enough. - -## Step 4: Verify and run your first prompt - -Open a new chat and click the tool picker. With a read-only key you'll see `list_status_pages`, `list_status_reports`, `list_maintenances`. With a read & write key you'll also see `create_status_report`, `add_status_report_update`, `update_status_report`, `resolve_status_report`, and `create_maintenance`. - -Try a read tool first: - -``` -List my openstatus status pages -``` - -Claude calls `list_status_pages`, asks for approval, and returns the slugs and ids in your workspace. With a write-scoped key, draft an incident — Claude shows you the title, status, message, and notify choice **before** firing: - -``` -Draft a status report on my "api" page: investigating elevated latency on the payment endpoint, do not notify subscribers yet -``` +The key lives in `env`, not inlined in `args`, because `mcp-remote` parses `--header` arguments before shell expansion. Quit Claude Desktop fully (`Cmd+Q` on macOS, **File > Quit** on Windows) and relaunch; MCP servers from the config file only load at launch. ## Built-in safeguards -Three properties make this safe enough for a real incident: +Four properties make this safe enough for a real incident: +- **Workspace-scoped consent** — the token only reaches the workspace you approved, with the access level you chose. A connector that asks for write access can be downgraded to read-only on the consent screen. - **Required `notify: boolean`** on every mutation tool — the schema rejects calls that omit it, so the LLM has to ask before firing. `notify: false` is a hard guarantee that no subscriber alert goes out. - **Lookup-before-mutate** — `create_status_report` and `create_maintenance` require a `pageId` resolved via `list_status_pages` rather than guessed. -- **Audit log attribution** — every mutation lands in the audit log under `actor_type = 'mcp'`, with `actor_id` (key) and `actor_user_id` (the user who created the key), so AI-driven actions are traceable as a separate slice. +- **Audit log attribution** — every mutation lands in the audit log under `actor_type = 'mcp'`, with `actor_id` (the connection) and `actor_user_id` (the user who approved it), so AI-driven actions are traceable as a separate slice. Connecting and revoking are audited too. + +## Disconnecting + +Open **Settings > Integrations > Connected apps** in the openstatus dashboard. Each connection shows the client, its access, who connected it, and when it was last used. Revoke it and the connector's next call fails; reconnect from Claude Desktop's **Settings > Connectors**. Members revoke their own connections; owners and admins can revoke any. Removing a member from the workspace revokes their connections automatically. ## Troubleshooting -- **Tools don't appear after restart.** Validate the JSON (a trailing comma silently disables the whole `mcpServers` block) and confirm `npx` is on the PATH that Claude Desktop sees — `nvm`-managed Node sometimes isn't. Use the official nodejs.org installer if `node --version` is missing in your shell. Check **Settings → Developer → Open Logs Folder** for `mcp-remote` stderr. -- **401 errors when calling a tool.** Verify the `x-openstatus-key` value matches a key in **Settings → API Tokens**. Sanity-check with `curl -i -H "x-openstatus-key: os_..." https://api.openstatus.dev/mcp`. +- **The connector shows as disconnected.** Open **Settings > Connectors**, click openstatus, and reconnect. This is expected after a revoke from the dashboard. +- **Tools don't show up in a chat.** Connectors are enabled per chat: open the tool picker and switch openstatus on. +- **"Tool not available" when creating an incident.** Your connection is read-only. Reconnect and pick **Read & write** on the consent screen. +- **`mcp-remote` tools don't appear after restart.** Validate the JSON (a trailing comma silently disables the whole `mcpServers` block) and confirm `npx` is on the PATH that Claude Desktop sees — `nvm`-managed Node sometimes isn't. Check **Settings > Developer > Open Logs Folder** for `mcp-remote` stderr. ## What's next? -- **[MCP Server Reference](https://www.openstatus.dev/docs/reference/mcp-server)** — full tool schema, error codes, and scope mechanics. +- **[MCP Server Reference](/docs/reference/mcp-server)** — full tool schema, error codes, OAuth endpoints, and scope mechanics. - **[Connect to Claude Code](/guides/connect-openstatus-to-claude-code)** — terminal-shaped variant of this guide. +- **[Connect to ChatGPT](/guides/connect-openstatus-to-chatgpt)** — the same setup for OpenAI's chat client. - **[Openstatus CLI](/tooling/cli)** — for terminal workflows that don't need an LLM in the loop. - **[Slack Agent](/status-page#slack-agent)** — for teams that live in Slack. @@ -146,25 +151,25 @@ Three properties make this safe enough for a real incident:
    -Yes, but indirectly. Claude Desktop launches MCP servers as local stdio processes, so a remote HTTP server like openstatus has to be bridged through `npx mcp-remote` — a small open-source proxy that translates Claude Desktop's stdio calls into HTTP requests. You configure `mcp-remote` once in `claude_desktop_config.json` and Claude Desktop treats openstatus like any other local server. +Yes. Claude Desktop and Claude.ai share a connector list. Add openstatus under **Settings > Connectors > Add custom connector** with the URL `https://api.openstatus.dev/mcp`; the connection is authorized with OAuth in your browser. No local process, no Node.js, no config file.
    -
    +
    -On macOS, the file lives at `~/Library/Application Support/Claude/claude_desktop_config.json`. On Windows, it lives at `%APPDATA%\Claude\claude_desktop_config.json`. Create the file if it does not exist. +No. The openstatus MCP server is an OAuth 2.1 authorization server. Claude Desktop opens a consent screen where you pick a workspace and an access level; tokens are issued and refreshed automatically. If you prefer an API key, you can still bridge the server through `npx mcp-remote` with an `x-openstatus-key` header in `claude_desktop_config.json`.
    -
    +
    -No. Authentication is the standard `x-openstatus-key` header — the same API key the openstatus CLI, REST API, and Terraform provider use. There is no MCP-specific OAuth flow. +In the openstatus dashboard under **Settings > Integrations > Connected apps**. Revoking signs the connector out immediately; reconnect from Claude Desktop's connector settings. Members revoke their own connections, owners and admins can revoke any.
    -
    +
    -The two most common causes are not fully quitting Claude Desktop (it only loads MCP servers at launch — close the window is not enough, you have to Cmd+Q on macOS or File → Quit on Windows) and `npx` not being on the PATH that Claude Desktop sees. Install Node.js 18+ from nodejs.org if `node --version` is missing in your shell. +Connectors are enabled per chat. Open the tool picker in the chat and switch the openstatus connector on. If the connector shows as disconnected in **Settings > Connectors**, click it and reconnect.
    @@ -176,13 +181,13 @@ No. Every publishing tool (`create_status_report`, `add_status_report_update`, `
    -Create an API key with `read` scope. The MCP server filters write tools out of the `tools/list` response for read-only keys, so Claude Desktop sees only the 14 read tools — status pages and page components, status reports, maintenances, monitors, response logs, notifications, private locations, and audit logs. None of the `create_*`, `update_*`, or `resolve_*` tools are registered for that session. +Pick **Read-only** on the consent screen. The MCP server filters write tools out of the `tools/list` response for read-only connections, so Claude Desktop sees only the 14 read tools — status pages and page components, status reports, maintenances, monitors, response logs, notifications, private locations, and audit logs. None of the `create_*`, `update_*`, or `resolve_*` tools are registered for that session.
    -Every mutation routed through Claude Desktop lands in the audit log under `actor_type = 'mcp'`, with `actor_id` set to the API key id and `actor_user_id` set to the user who created the key. This separates AI-driven actions from CLI, dashboard, and direct API mutations. +Every mutation routed through Claude Desktop lands in the audit log under `actor_type = 'mcp'`, with `actor_id` set to the connection (`oat_` for OAuth, the key id for an API key) and `actor_user_id` set to the user who approved the connection or created the key. This separates AI-driven actions from CLI, dashboard, and direct API mutations.
    diff --git a/apps/web/src/content/pages/guides/connect-openstatus-to-codex.mdx b/apps/web/src/content/pages/guides/connect-openstatus-to-codex.mdx new file mode 100644 index 000000000..4c3541cc8 --- /dev/null +++ b/apps/web/src/content/pages/guides/connect-openstatus-to-codex.mdx @@ -0,0 +1,246 @@ +--- +title: "Connect Openstatus to Codex (MCP Setup Guide)" +author: "openstatus" +description: "Wire the openstatus MCP server into OpenAI Codex so the agent can list status pages, draft incidents, and schedule maintenance from your terminal — sign in with OAuth, no key to paste, with audit-logged mutations and explicit notify semantics." +publishedAt: "2026-09-08" +updatedAt: "2026-09-08" +category: "education" +howto: + totalTime: "PT2M" + steps: + - name: "Register the MCP server with Codex" + text: "Run `codex mcp add openstatus --url https://api.openstatus.dev/mcp` to attach the openstatus tools to every Codex session." + url: "#step-1-register-the-mcp-server" + - name: "Sign in with OAuth" + text: "Run `codex mcp login openstatus`. Approve the connection in the browser, picking a workspace and read-only or read & write access." + url: "#step-2-sign-in" + - name: "Verify the connection" + text: "Run `codex mcp list` or type /mcp inside Codex and confirm openstatus is listed as enabled with the expected tools (list_status_pages, list_status_reports, …)." + url: "#step-3-verify-the-connection" + - name: "Run your first prompt" + text: "Ask Codex to list your status pages or draft an incident. The agent asks for approval before each tool call." + url: "#step-4-run-your-first-prompt" +faq: + - question: "Does Codex support remote MCP servers like openstatus?" + answer: "Yes. Codex supports stdio and Streamable HTTP MCP servers. The openstatus MCP server is a stateless Streamable HTTP endpoint, registered with `codex mcp add openstatus --url https://api.openstatus.dev/mcp`." + - question: "What can Codex do once the openstatus MCP server is connected?" + answer: "Codex can list status pages, status reports, monitors, and maintenance windows; create new status reports; append updates to existing reports; resolve reports; edit metadata; and schedule maintenance windows — all scoped to the workspace you picked when you approved the connection." + - question: "Do I need an API key to connect Codex?" + answer: "No. The openstatus MCP server is an OAuth 2.1 authorization server. `codex mcp login openstatus` opens a consent screen in your browser where you pick a workspace and an access level; tokens are issued and refreshed automatically. An API key in the `x-openstatus-key` header still works for CI and headless agents." + - question: "Does the same setup work in the ChatGPT desktop app and the Codex IDE extension?" + answer: "Yes. The Codex CLI, the Codex IDE extension, and the ChatGPT desktop app share one MCP configuration in `~/.codex/config.toml`. Register openstatus once and every surface picks it up; sign in once and the token is reused." + - question: "Can Codex accidentally notify subscribers when creating an incident?" + answer: "No. Every publishing tool (`create_status_report`, `add_status_report_update`, `resolve_status_report`, `create_maintenance`) requires an explicit `notify: true | false` field. Codex must show the notify choice before firing the tool, so an LLM cannot quietly fan out an alert by omitting the flag. `update_status_report` is metadata-only and has no notify path at all." + - question: "How do I give Codex read-only access to my workspace?" + answer: "Pick Read-only on the consent screen. The MCP server filters write tools out of the `tools/list` response for read-only connections, so Codex sees only the 14 read tools — status pages and page components, status reports, maintenances, monitors, response logs, notifications, private locations, and audit logs. None of the `create_*`, `update_*`, or `resolve_*` tools are registered for that session." + - question: "How do I disconnect Codex from my workspace?" + answer: "Open Settings > Integrations > Connected apps in the openstatus dashboard and revoke the connection. Codex's next call fails with 401; run `codex mcp login openstatus` to reconnect. Members revoke their own connections, owners and admins can revoke any." + - question: "Where do MCP-driven changes appear in the audit log?" + answer: "Every mutation routed through the MCP server lands in the audit log under `actor_type = 'mcp'`, with `actor_id` set to the connection (`oat_` for OAuth, the key id for an API key) and `actor_user_id` set to the user who approved the connection or created the key. This separates AI-driven actions from CLI, dashboard, and direct API mutations." + - question: "Can I commit the MCP configuration to my repository?" + answer: "Yes. Add the `[mcp_servers.openstatus]` block to a project-scoped `.codex/config.toml` at the repo root with no header; Codex reads it for trusted projects and each teammate signs in with OAuth on first use. For CI, use `env_http_headers` to read `x-openstatus-key` from an environment variable so the secret stays out of source control." +--- + +**TL;DR** — Run one `codex mcp add` command, sign in with `codex mcp login`, and Codex can drive your status pages: list incidents, draft updates, and schedule maintenance from your terminal. No key to paste. Every mutation is audit-logged under `actor_type = 'mcp'` and notifications never fire unless you explicitly approve. + +--- + +[Codex](https://developers.openai.com/codex) is OpenAI's coding agent for the terminal, your IDE, and the ChatGPT desktop app. The [Model Context Protocol](https://modelcontextprotocol.io) (MCP) lets it reach out to remote services like openstatus — with the agent's host responsible for approval and audit. This guide walks through the two-minute setup. + +## Why connect Openstatus to Codex? + +Status page work usually lands while you're already debugging in a terminal. Connecting openstatus to Codex keeps the writing side of the workflow there too: + +- **During an incident** — draft an investigating-status update for the right page and approve it without opening the dashboard. +- **At deploy time** — schedule a maintenance window from the same Codex session you used to ship the change. +- **Read-only audits** — a teammate connects with read-only access; their Codex can summarize page health and monitor status with zero mutation risk. + +The same server works with [ChatGPT](/guides/connect-openstatus-to-chatgpt), [Claude Code](/guides/connect-openstatus-to-claude-code), [Claude Desktop](/guides/connect-openstatus-to-claude-desktop), [opencode](/guides/connect-openstatus-to-opencode), Cursor, and any other MCP-compatible client. + +## Prerequisites + +- An [openstatus account](https://www.openstatus.dev) with at least one status page. +- Codex CLI installed and signed in (`codex --version` should print a version). + +## Step 1: Register the MCP server + +Codex talks to the openstatus MCP server over Streamable HTTP. Register it with `codex mcp add`: + +```bash +codex mcp add openstatus --url https://api.openstatus.dev/mcp +``` + +`--url` tells Codex this is a remote HTTP endpoint rather than a local stdio process. The command writes the server to `~/.codex/config.toml`, which every Codex surface on your machine reads — the CLI, the IDE extension, and the ChatGPT desktop app. + +### Config file alternative (`config.toml`) + +If you'd rather edit the file directly, add this block to `~/.codex/config.toml`: + +```toml +[mcp_servers.openstatus] +url = "https://api.openstatus.dev/mcp" +``` + +To commit the configuration to a repo so your team picks it up automatically, put the same block in a project-scoped `.codex/config.toml` at the project root. Codex only loads project config for trusted projects. No secret in the file: each teammate signs in with OAuth the first time they use it. + +## Step 2: Sign in + +Run: + +```bash +codex mcp login openstatus +``` + +Your browser opens the openstatus consent screen: + +- **Workspace** — the workspace this connection can reach. If you belong to several, pick one; log in again to switch. +- **Access** — **Read-only** lets Codex list status pages, reports, monitors, and maintenance windows. **Read & write** also lets it create reports, append updates, resolve incidents, and schedule maintenance. + +Click **Approve**. Codex receives a token and refreshes it in the background; you won't be asked again unless the connection is revoked. + +You can also sign in from inside a session: type `/mcp`, and choose **Authenticate** next to openstatus. + +## Step 3: Verify the connection + +Run: + +```bash +codex mcp list +``` + +`openstatus` should be listed as enabled with no pending sign-in. Inside a Codex session, `/mcp` shows the same list with the tools each server exposes. With read-only access you'll see `list_status_pages`, `list_status_reports`, `list_maintenances`, `list_monitors`, and the other read tools. With read & write you'll also see `create_status_report`, `add_status_report_update`, `update_status_report`, `resolve_status_report`, and `create_maintenance`. The server filters mutation tools out for read-only connections, so a misconfigured client cannot call them. + +## Step 4: Run your first prompt + +Try a read tool first: + +``` +> list my openstatus status pages +``` + +Codex calls `list_status_pages`, asks for your approval if your approval policy requires it, and returns the slugs and ids in your workspace. + +With write access, draft an incident — Codex shows you the title, status, message, and notify choice **before** firing: + +``` +> draft a status report on my "api" page: investigating elevated latency on the payment endpoint, do not notify subscribers yet +``` + +The agent resolves the page id via `list_status_pages`, presents the draft, and only calls `create_status_report` once you confirm. + +## Headless setups: use an API key + +OAuth needs a browser on the same machine. In CI, on a remote box, or inside a container, pass an API key instead. Create one in **Settings > API Tokens** (scopes are immutable on a key) and add the header: + +```bash +codex mcp add openstatus \ + --url https://api.openstatus.dev/mcp \ + --header "x-openstatus-key=os_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +``` + +In a committed `.codex/config.toml`, read the key from the environment so it never lands in version control: + +```toml +[mcp_servers.openstatus] +url = "https://api.openstatus.dev/mcp" + +[mcp_servers.openstatus.env_http_headers] +"x-openstatus-key" = "OPENSTATUS_API_KEY" +``` + +Export `OPENSTATUS_API_KEY` in your shell and Codex resolves it at connection time. When a header is present, the OAuth sign-in is skipped. + +## Built-in safeguards + +Four properties make this safe enough for a real incident: + +- **Workspace-scoped consent** — the token only reaches the workspace you approved, with the access level you chose. You can downgrade a client that asks for write access to read-only on the consent screen. +- **Required `notify: boolean`** on every mutation tool — the schema rejects calls that omit it, so the LLM has to ask before firing. `notify: false` is a hard guarantee that no subscriber alert goes out. +- **Lookup-before-mutate** — `create_status_report` and `create_maintenance` require a `pageId` resolved via `list_status_pages` rather than guessed. +- **Audit log attribution** — every mutation lands in the audit log under `actor_type = 'mcp'`, with `actor_id` (the connection) and `actor_user_id` (the user who approved it), so AI-driven actions are traceable as a separate slice. Connecting and revoking are audited too. + +## Disconnecting + +Open **Settings > Integrations > Connected apps** in the dashboard. Each connection shows the client, its access, who connected it, and when it was last used. Revoke it and Codex's next call fails with `401`; run `codex mcp login openstatus` to reconnect. Members revoke their own connections; owners and admins can revoke any. Removing a member from the workspace revokes their connections automatically. + +To drop the credential on your side without touching the dashboard, run `codex mcp logout openstatus`. To remove the server entirely, run `codex mcp remove openstatus`. + +## Troubleshooting + +- **`codex mcp list` shows openstatus as needing sign-in.** Run `codex mcp login openstatus` again. Sanity-check reachability with `curl -i -H "Accept: text/event-stream" https://api.openstatus.dev/mcp` — a `401` with a `WWW-Authenticate` header means the server is up and waiting for a sign-in. +- **The browser never opens.** Codex prints the authorization URL; open it in a browser on the same machine, since the callback lands on a `127.0.0.1` port Codex listens on. For remote sessions, use an [API key](#headless-setups-use-an-api-key). +- **"Tool not available" when creating an incident.** Your connection is read-only. Sign in again and pick **Read & write**. +- **Tools don't appear after editing `config.toml`.** Codex reads the file at startup; start a new session. In the ChatGPT desktop app, save the server and choose **Restart**. + +## What's next? + +- **[MCP Server Reference](/docs/reference/mcp-server)** — full tool schema, error codes, OAuth endpoints, and scope mechanics. +- **[Connect to ChatGPT](/guides/connect-openstatus-to-chatgpt)** — chat-shaped variant of this guide. +- **[Connect to Claude Code](/guides/connect-openstatus-to-claude-code)** — the same setup for Anthropic's terminal agent. +- **[Connect to opencode](/guides/connect-openstatus-to-opencode)** — the same setup for the open-source terminal agent. +- **[Openstatus CLI](/tooling/cli)** — for terminal workflows that don't need an LLM in the loop. +- **[Slack Agent](/status-page#slack-agent)** — for teams that live in Slack. + +## Frequently asked questions + +
    + +Yes. Codex supports stdio and Streamable HTTP MCP servers. The openstatus MCP server is a stateless Streamable HTTP endpoint, registered with `codex mcp add openstatus --url https://api.openstatus.dev/mcp`. + +
    + +
    + +Codex can list status pages, status reports, monitors, and maintenance windows; create new status reports; append updates to existing reports; resolve reports; edit metadata; and schedule maintenance windows — all scoped to the workspace you picked when you approved the connection. + +
    + +
    + +No. The openstatus MCP server is an OAuth 2.1 authorization server. `codex mcp login openstatus` opens a consent screen in your browser where you pick a workspace and an access level; tokens are issued and refreshed automatically. An API key in the `x-openstatus-key` header still works for CI and headless agents. + +
    + +
    + +Yes. The Codex CLI, the Codex IDE extension, and the ChatGPT desktop app share one MCP configuration in `~/.codex/config.toml`. Register openstatus once and every surface picks it up; sign in once and the token is reused. + +
    + +
    + +No. Every publishing tool (`create_status_report`, `add_status_report_update`, `resolve_status_report`, `create_maintenance`) requires an explicit `notify: true | false` field. Codex must show the notify choice before firing the tool, so an LLM cannot quietly fan out an alert by omitting the flag. `update_status_report` is metadata-only and has no notify path at all. + +
    + +
    + +Pick **Read-only** on the consent screen. The MCP server filters write tools out of the `tools/list` response for read-only connections, so Codex sees only the 14 read tools — status pages and page components, status reports, maintenances, monitors, response logs, notifications, private locations, and audit logs. None of the `create_*`, `update_*`, or `resolve_*` tools are registered for that session. + +
    + +
    + +Open **Settings > Integrations > Connected apps** in the openstatus dashboard and revoke the connection. Codex's next call fails with `401`; run `codex mcp login openstatus` to reconnect. Members revoke their own connections, owners and admins can revoke any. + +
    + +
    + +Every mutation routed through the MCP server lands in the audit log under `actor_type = 'mcp'`, with `actor_id` set to the connection (`oat_` for OAuth, the key id for an API key) and `actor_user_id` set to the user who approved the connection or created the key. This separates AI-driven actions from CLI, dashboard, and direct API mutations. + +
    + +
    + +Yes. Add the `[mcp_servers.openstatus]` block to a project-scoped `.codex/config.toml` at the repo root with no header; Codex reads it for trusted projects and each teammate signs in with OAuth on first use. For CI, use `env_http_headers` to read `x-openstatus-key` from an environment variable so the secret stays out of source control. + +
    + +--- + +Start free. No credit card required. Configure Codex to drive your status pages in under 5 minutes. + +Try openstatus free + +--- diff --git a/apps/web/src/content/pages/guides/connect-openstatus-to-opencode.mdx b/apps/web/src/content/pages/guides/connect-openstatus-to-opencode.mdx new file mode 100644 index 000000000..37ad8315a --- /dev/null +++ b/apps/web/src/content/pages/guides/connect-openstatus-to-opencode.mdx @@ -0,0 +1,247 @@ +--- +title: "Connect Openstatus to opencode (MCP Setup Guide)" +author: "openstatus" +description: "Wire the openstatus MCP server into opencode so the agent can list status pages, draft incidents, and schedule maintenance from your terminal — sign in with OAuth, no key to paste, with audit-logged mutations and explicit notify semantics." +publishedAt: "2026-09-08" +updatedAt: "2026-09-08" +category: "education" +howto: + totalTime: "PT2M" + steps: + - name: "Add the MCP server to opencode.json" + text: "Add an `openstatus` entry with `type: remote` and the URL `https://api.openstatus.dev/mcp` to your project `opencode.json` or the global `~/.config/opencode/opencode.json`." + url: "#step-1-add-the-mcp-server" + - name: "Sign in with OAuth" + text: "Run `opencode mcp auth openstatus`. Approve the connection in the browser, picking a workspace and read-only or read & write access." + url: "#step-2-sign-in" + - name: "Verify the connection" + text: "Run `opencode mcp list` and confirm openstatus is connected and authenticated." + url: "#step-3-verify-the-connection" + - name: "Run your first prompt" + text: "Ask opencode to list your status pages or draft an incident. The agent asks for approval before each tool call." + url: "#step-4-run-your-first-prompt" +faq: + - question: "Does opencode support remote MCP servers like openstatus?" + answer: "Yes. opencode supports local (stdio) and remote MCP servers. The openstatus MCP server is a stateless Streamable HTTP endpoint, added as an entry with `\"type\": \"remote\"` and `\"url\": \"https://api.openstatus.dev/mcp\"` under the `mcp` key of your opencode config." + - question: "What can opencode do once the openstatus MCP server is connected?" + answer: "opencode can list status pages, status reports, monitors, and maintenance windows; create new status reports; append updates to existing reports; resolve reports; edit metadata; and schedule maintenance windows — all scoped to the workspace you picked when you approved the connection." + - question: "Do I need an API key to connect opencode?" + answer: "No. The openstatus MCP server is an OAuth 2.1 authorization server. opencode starts the flow when the server answers 401, or on demand with `opencode mcp auth openstatus`; you pick a workspace and an access level on the consent screen, and tokens are issued and refreshed automatically. An API key in the `x-openstatus-key` header still works for CI and headless agents." + - question: "Where does opencode store the OAuth tokens?" + answer: "In `~/.local/share/opencode/mcp-auth.json`, outside your project. Nothing sensitive lands in `opencode.json`, so the config file is safe to commit and share with your team — each teammate signs in with their own account on first use." + - question: "Can opencode accidentally notify subscribers when creating an incident?" + answer: "No. Every publishing tool (`create_status_report`, `add_status_report_update`, `resolve_status_report`, `create_maintenance`) requires an explicit `notify: true | false` field. opencode must show the notify choice before firing the tool, so an LLM cannot quietly fan out an alert by omitting the flag. `update_status_report` is metadata-only and has no notify path at all." + - question: "How do I give opencode read-only access to my workspace?" + answer: "Pick Read-only on the consent screen. The MCP server filters write tools out of the `tools/list` response for read-only connections, so opencode sees only the read tools — status pages and page components, status reports, maintenances, monitors, response logs, notifications, private locations, and audit logs. None of the `create_*`, `update_*`, or `resolve_*` tools are registered for that session." + - question: "How do I disconnect opencode from my workspace?" + answer: "Open Settings > Integrations > Connected apps in the openstatus dashboard and revoke the connection. opencode's next call fails with 401; run `opencode mcp auth openstatus` to reconnect. Members revoke their own connections, owners and admins can revoke any. To drop the local credential without touching the dashboard, run `opencode mcp logout openstatus`." + - question: "Where do MCP-driven changes appear in the audit log?" + answer: "Every mutation routed through the MCP server lands in the audit log under `actor_type = 'mcp'`, with `actor_id` set to the connection (`oat_` for OAuth, the key id for an API key) and `actor_user_id` set to the user who approved the connection or created the key. This separates AI-driven actions from CLI, dashboard, and direct API mutations." + - question: "Can I turn the server off without deleting the config?" + answer: "Yes. Set `\"enabled\": false` on the `openstatus` entry and opencode skips it at startup. Flip it back to `true` when you need the tools again; the stored credential survives." +--- + +**TL;DR** — Add six lines to `opencode.json`, run `opencode mcp auth openstatus`, and opencode can drive your status pages: list incidents, draft updates, and schedule maintenance from your terminal. No key to paste. Every mutation is audit-logged under `actor_type = 'mcp'` and notifications never fire unless you explicitly approve. + +--- + +[opencode](https://opencode.ai) is an open-source coding agent for the terminal, model-agnostic and configured from a single JSON file. The [Model Context Protocol](https://modelcontextprotocol.io) (MCP) lets it reach out to remote services like openstatus — with the agent's host responsible for approval and audit. This guide walks through the two-minute setup. + +## Why connect Openstatus to opencode? + +Status page work usually lands while you're already debugging in a terminal. Connecting openstatus to opencode keeps the writing side of the workflow there too: + +- **During an incident** — draft an investigating-status update for the right page and approve it without opening the dashboard. +- **At deploy time** — schedule a maintenance window from the same opencode session you used to ship the change. +- **Read-only audits** — a teammate connects with read-only access; their opencode can summarize page health and monitor status with zero mutation risk. + +The same server works with [Claude Code](/guides/connect-openstatus-to-claude-code), [Claude Desktop](/guides/connect-openstatus-to-claude-desktop), [Codex](/guides/connect-openstatus-to-codex), [ChatGPT](/guides/connect-openstatus-to-chatgpt), Cursor, and any other MCP-compatible client. + +## Prerequisites + +- An [openstatus account](https://www.openstatus.dev) with at least one status page. +- opencode installed (`opencode --version` should print a version). + +## Step 1: Add the MCP server + +opencode has no `mcp add` command — remote servers are declared in the config file. Add an `openstatus` entry under the `mcp` key of `opencode.json` at your project root, or `~/.config/opencode/opencode.json` to make it available in every project: + +```json +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "openstatus": { + "type": "remote", + "url": "https://api.openstatus.dev/mcp", + "enabled": true + } + } +} +``` + +`"type": "remote"` tells opencode this is an HTTP endpoint rather than a local process to spawn. `"enabled": false` is the switch to park the server without deleting the entry. + +The project file is safe to commit: it holds no secret. Tokens live in `~/.local/share/opencode/mcp-auth.json` on each machine, so every teammate signs in with their own openstatus account the first time they use it. + +## Step 2: Sign in + +Run: + +```bash +opencode mcp auth openstatus +``` + +opencode also starts this flow on its own the first time the server answers `401`, so you can skip straight to a prompt and authenticate when asked. + +Your browser opens the openstatus consent screen: + +- **Workspace** — the workspace this connection can reach. If you belong to several, pick one; authenticate again to switch. +- **Access** — **Read-only** lets opencode list status pages, reports, monitors, and maintenance windows. **Read & write** also lets it create reports, append updates, resolve incidents, and schedule maintenance. + +Click **Approve**. opencode stores the token and refreshes it in the background; you won't be asked again unless the connection is revoked. + +## Step 3: Verify the connection + +Run: + +```bash +opencode mcp list +``` + +`openstatus` should be listed as connected and authenticated. With read-only access the session exposes `list_status_pages`, `list_status_reports`, `list_maintenances`, `list_monitors`, and the other read tools. With read & write you also get `create_status_report`, `add_status_report_update`, `update_status_report`, `resolve_status_report`, and `create_maintenance`. The server filters mutation tools out for read-only connections, so a misconfigured client cannot call them. + +If something looks wrong, `opencode mcp debug openstatus` prints the connection and OAuth diagnostics. + +## Step 4: Run your first prompt + +Try a read tool first: + +``` +> list my openstatus status pages +``` + +opencode calls `list_status_pages`, asks for your approval, and returns the slugs and ids in your workspace. + +With write access, draft an incident — opencode shows you the title, status, message, and notify choice **before** firing: + +``` +> draft a status report on my "api" page: investigating elevated latency on the payment endpoint, do not notify subscribers yet +``` + +The agent resolves the page id via `list_status_pages`, presents the draft, and only calls `create_status_report` once you confirm. + +## Headless setups: use an API key + +OAuth needs a browser on the same machine. In CI, on a remote box, or inside a container, pass an API key instead. Create one in **Settings > API Tokens** (scopes are immutable on a key) and add it as a header. opencode substitutes `{env:VAR}` in config values, so the secret stays out of the file: + +```json +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "openstatus": { + "type": "remote", + "url": "https://api.openstatus.dev/mcp", + "enabled": true, + "headers": { + "x-openstatus-key": "{env:OPENSTATUS_API_KEY}" + }, + "oauth": false + } + } +} +``` + +Export `OPENSTATUS_API_KEY` in the environment and opencode resolves it at connection time. `"oauth": false` stops opencode from trying to open a browser when a call is rejected. + +## Built-in safeguards + +Four properties make this safe enough for a real incident: + +- **Workspace-scoped consent** — the token only reaches the workspace you approved, with the access level you chose. You can downgrade a client that asks for write access to read-only on the consent screen. +- **Required `notify: boolean`** on every mutation tool — the schema rejects calls that omit it, so the LLM has to ask before firing. `notify: false` is a hard guarantee that no subscriber alert goes out. +- **Lookup-before-mutate** — `create_status_report` and `create_maintenance` require a `pageId` resolved via `list_status_pages` rather than guessed. +- **Audit log attribution** — every mutation lands in the audit log under `actor_type = 'mcp'`, with `actor_id` (the connection) and `actor_user_id` (the user who approved it), so AI-driven actions are traceable as a separate slice. Connecting and revoking are audited too. + +## Disconnecting + +Open **Settings > Integrations > Connected apps** in the dashboard. Each connection shows the client, its access, who connected it, and when it was last used. Revoke it and opencode's next call fails with `401`; run `opencode mcp auth openstatus` to reconnect. Members revoke their own connections; owners and admins can revoke any. Removing a member from the workspace revokes their connections automatically. + +To drop the credential on your side without touching the dashboard, run `opencode mcp logout openstatus`. To remove the server entirely, delete the `openstatus` entry from your config. + +## Troubleshooting + +- **`opencode mcp list` shows openstatus as unauthenticated.** Run `opencode mcp auth openstatus` again. Sanity-check reachability with `curl -i -H "Accept: text/event-stream" https://api.openstatus.dev/mcp` — a `401` with a `WWW-Authenticate` header means the server is up and waiting for a sign-in. +- **The browser never opens.** opencode prints the authorization URL; open it in a browser on the same machine, since the callback lands on a loopback port opencode listens on. For remote sessions, use an [API key](#headless-setups-use-an-api-key). +- **"Tool not available" when creating an incident.** Your connection is read-only. Authenticate again and pick **Read & write**. +- **Tools don't appear after editing the config.** opencode reads the config at startup; start a new session. Check `"enabled"` is `true` and that the JSON parses. +- **The server times out while listing tools.** Raise `"timeout"` on the entry; it defaults to 5000 milliseconds. + +## What's next? + +- **[MCP Server Reference](/docs/reference/mcp-server)** — full tool schema, error codes, OAuth endpoints, and scope mechanics. +- **[Connect to Claude Code](/guides/connect-openstatus-to-claude-code)** — the same setup for Anthropic's terminal agent. +- **[Connect to Codex](/guides/connect-openstatus-to-codex)** — the same setup for OpenAI's terminal agent. +- **[Openstatus CLI](/tooling/cli)** — for terminal workflows that don't need an LLM in the loop. +- **[Slack Agent](/status-page#slack-agent)** — for teams that live in Slack. + +## Frequently asked questions + +
    + +Yes. opencode supports local (stdio) and remote MCP servers. The openstatus MCP server is a stateless Streamable HTTP endpoint, added as an entry with `"type": "remote"` and `"url": "https://api.openstatus.dev/mcp"` under the `mcp` key of your opencode config. + +
    + +
    + +opencode can list status pages, status reports, monitors, and maintenance windows; create new status reports; append updates to existing reports; resolve reports; edit metadata; and schedule maintenance windows — all scoped to the workspace you picked when you approved the connection. + +
    + +
    + +No. The openstatus MCP server is an OAuth 2.1 authorization server. opencode starts the flow when the server answers `401`, or on demand with `opencode mcp auth openstatus`; you pick a workspace and an access level on the consent screen, and tokens are issued and refreshed automatically. An API key in the `x-openstatus-key` header still works for CI and headless agents. + +
    + +
    + +In `~/.local/share/opencode/mcp-auth.json`, outside your project. Nothing sensitive lands in `opencode.json`, so the config file is safe to commit and share with your team — each teammate signs in with their own account on first use. + +
    + +
    + +No. Every publishing tool (`create_status_report`, `add_status_report_update`, `resolve_status_report`, `create_maintenance`) requires an explicit `notify: true | false` field. opencode must show the notify choice before firing the tool, so an LLM cannot quietly fan out an alert by omitting the flag. `update_status_report` is metadata-only and has no notify path at all. + +
    + +
    + +Pick **Read-only** on the consent screen. The MCP server filters write tools out of the `tools/list` response for read-only connections, so opencode sees only the read tools — status pages and page components, status reports, maintenances, monitors, response logs, notifications, private locations, and audit logs. None of the `create_*`, `update_*`, or `resolve_*` tools are registered for that session. + +
    + +
    + +Open **Settings > Integrations > Connected apps** in the openstatus dashboard and revoke the connection. opencode's next call fails with `401`; run `opencode mcp auth openstatus` to reconnect. Members revoke their own connections, owners and admins can revoke any. To drop the local credential without touching the dashboard, run `opencode mcp logout openstatus`. + +
    + +
    + +Every mutation routed through the MCP server lands in the audit log under `actor_type = 'mcp'`, with `actor_id` set to the connection (`oat_` for OAuth, the key id for an API key) and `actor_user_id` set to the user who approved the connection or created the key. This separates AI-driven actions from CLI, dashboard, and direct API mutations. + +
    + +
    + +Yes. Set `"enabled": false` on the `openstatus` entry and opencode skips it at startup. Flip it back to `true` when you need the tools again; the stored credential survives. + +
    + +--- + +Start free. No credit card required. Configure opencode to drive your status pages in under 5 minutes. + +Try openstatus free diff --git a/apps/web/src/content/pages/guides/migrate-from-atlassian-statuspage.mdx b/apps/web/src/content/pages/guides/migrate-from-atlassian-statuspage.mdx index a310c7d5f..75ea321b9 100644 --- a/apps/web/src/content/pages/guides/migrate-from-atlassian-statuspage.mdx +++ b/apps/web/src/content/pages/guides/migrate-from-atlassian-statuspage.mdx @@ -19,7 +19,7 @@ howto: totalTime: "PT2M" steps: - name: "Get your Statuspage API key" - text: "Go to your Atlassian Statuspage account → Manage Account → API. Copy your API key." + text: "Go to your Atlassian Statuspage account > Manage Account > API. Copy your API key." - name: "Preview the import" text: "In your openstatus dashboard, go to Status Pages, select a status page, open the Components tab, and scroll down to the Import section. Select Atlassian Statuspage, paste your API key, and click Preview." - name: "Run the import" diff --git a/apps/web/src/content/pages/guides/migrate-from-betterstack.mdx b/apps/web/src/content/pages/guides/migrate-from-betterstack.mdx index 50866ff5f..8adffcbe4 100644 --- a/apps/web/src/content/pages/guides/migrate-from-betterstack.mdx +++ b/apps/web/src/content/pages/guides/migrate-from-betterstack.mdx @@ -21,7 +21,7 @@ howto: totalTime: "PT2M" steps: - name: "Get your Better Stack API token" - text: "Go to Better Stack → Settings → API tokens. Copy your Uptime API token (team-scoped, not the global API token)." + text: "Go to Better Stack > Settings > API tokens. Copy your Uptime API token (team-scoped, not the global API token)." - name: "Preview the import" text: "In your openstatus dashboard, go to Status Pages, select a status page, open the Components tab, and scroll down to the Import section. Select Better Stack, paste your API token, and click Preview." - name: "Run the import" diff --git a/apps/web/src/content/pages/guides/migrate-from-checkly.mdx b/apps/web/src/content/pages/guides/migrate-from-checkly.mdx index ea4488d5f..c92a9ae82 100644 --- a/apps/web/src/content/pages/guides/migrate-from-checkly.mdx +++ b/apps/web/src/content/pages/guides/migrate-from-checkly.mdx @@ -10,7 +10,7 @@ faq: - question: "Which Checkly check types are supported?" answer: "API and URL checks import as HTTP monitors. TCP, ICMP, DNS, and SSL map to their openstatus job types. Browser, Playwright, multi-step, heartbeat, gRPC, traceroute, and AI checks have no HTTP-family equivalent and are skipped with a reason in the preview." - question: "What credentials do I need?" - answer: "Two values: a Checkly API key (User Settings → API keys) and your Checkly account ID (Account Settings → General). Checkly requires the account ID on every API request, so both are mandatory." + answer: "Two values: a Checkly API key (User Settings > API keys) and your Checkly account ID (Account Settings > General). Checkly requires the account ID on every API request, so both are mandatory." - question: "How are Checkly locations mapped to openstatus?" answer: "Checkly's AWS-region locations are mapped to the nearest openstatus probe: us-east-1 → iad, us-west-2 → sea, eu-west-1 → lhr, eu-central-1 → fra, ap-southeast-1 → sin, ap-southeast-2 → syd, and so on. Unmapped locations fall back to iad." - question: "Do Checkly status-page services become monitored components?" @@ -21,7 +21,7 @@ howto: totalTime: "PT2M" steps: - name: "Get your Checkly API key and account ID" - text: "In Checkly, go to User Settings → API keys and create a key. Then open Account Settings → General and copy your account ID. You need both." + text: "In Checkly, go to User Settings > API keys and create a key. Then open Account Settings > General and copy your account ID. You need both." - name: "Preview the import" text: "In your openstatus dashboard, go to Status Pages, select a status page, open the Components tab, and scroll to the Import section. Select Checkly, paste your API key and account ID, then click Preview." - name: "Run the import" @@ -198,7 +198,7 @@ API and URL checks import as HTTP monitors. TCP, ICMP, DNS, and SSL map to their
    -Two values: a Checkly API key (User Settings → API keys) and your Checkly account ID (Account Settings → General). Checkly requires the account ID on every API request, so both are mandatory. +Two values: a Checkly API key (User Settings > API keys) and your Checkly account ID (Account Settings > General). Checkly requires the account ID on every API request, so both are mandatory.
    diff --git a/apps/web/src/content/pages/guides/migrate-from-instatus.mdx b/apps/web/src/content/pages/guides/migrate-from-instatus.mdx index 7615933b4..bc185de17 100644 --- a/apps/web/src/content/pages/guides/migrate-from-instatus.mdx +++ b/apps/web/src/content/pages/guides/migrate-from-instatus.mdx @@ -21,7 +21,7 @@ howto: totalTime: "PT2M" steps: - name: "Get your Instatus API key" - text: "Go to your Instatus dashboard → Settings → API. Copy your API key." + text: "Go to your Instatus dashboard > Settings > API. Copy your API key." - name: "Preview the import" text: "In your openstatus dashboard, go to Status Pages, select a status page, open the Components tab, and scroll down to the Import section. Select Instatus, paste your API key, and click Preview." - name: "Run the import" diff --git a/apps/web/src/content/pages/product/tooling/api.mdx b/apps/web/src/content/pages/product/tooling/api.mdx index 09457cf94..98229f55e 100644 --- a/apps/web/src/content/pages/product/tooling/api.mdx +++ b/apps/web/src/content/pages/product/tooling/api.mdx @@ -10,7 +10,7 @@ faq: - question: "Is the API REST or RPC?" answer: "It's ConnectRPC — JSON over HTTP, but with a typed RPC contract instead of REST conventions. Every method is a POST to /rpc/openstatus.v1./. You can call it from curl exactly like you'd call a REST endpoint." - question: "How do I authenticate?" - answer: "Pass the x-openstatus-key header on every request. Generate a key from Settings → API Tokens — the same key works for the CLI, Terraform provider, and MCP server." + answer: "Pass the x-openstatus-key header on every request. Generate a key from Settings > API Tokens — the same key works for the CLI, Terraform provider, and MCP server." - question: "Do you publish an OpenAPI spec?" answer: "Yes. The full schema is browsable at api.openstatus.dev/openapi and is the source of truth for the Node SDK and any generated clients." --- @@ -26,7 +26,7 @@ curl https://api.openstatus.dev/rpc/openstatus.v1.MonitorService/ListMonitors \ -d '{}' ``` -Generate a key from **Settings → API Tokens**. +Generate a key from **Settings > API Tokens**. ## What you can do @@ -86,7 +86,7 @@ It's ConnectRPC — JSON over HTTP, but with a typed RPC contract instead of RES
    -Pass the x-openstatus-key header on every request. Generate a key from Settings → API Tokens — the same key works for the CLI, Terraform provider, and MCP server. +Pass the x-openstatus-key header on every request. Generate a key from Settings > API Tokens — the same key works for the CLI, Terraform provider, and MCP server.
    diff --git a/apps/web/src/content/pages/product/tooling/cli.mdx b/apps/web/src/content/pages/product/tooling/cli.mdx index 0355647d3..aed15b3fb 100644 --- a/apps/web/src/content/pages/product/tooling/cli.mdx +++ b/apps/web/src/content/pages/product/tooling/cli.mdx @@ -58,7 +58,7 @@ You get incident communication templates, maintenance announcement drafts, perio ## Auth & audit -Authenticate with `openstatus login` (browser flow) or `OPENSTATUS_API_TOKEN` (CI). The token is the same one the API, Terraform provider, and MCP server share — generate it from **Settings → API Tokens**. +Authenticate with `openstatus login` (browser flow) or `OPENSTATUS_API_TOKEN` (CI). The token is the same one the API, Terraform provider, and MCP server share — generate it from **Settings > API Tokens**. Every mutation lands in the audit log under `actor_type = 'cli'`, so you can trace any change back to a key, a user, and a transport. diff --git a/apps/web/src/content/pages/product/tooling/mcp-server.mdx b/apps/web/src/content/pages/product/tooling/mcp-server.mdx index 9126c238e..e6a3d7cbc 100644 --- a/apps/web/src/content/pages/product/tooling/mcp-server.mdx +++ b/apps/web/src/content/pages/product/tooling/mcp-server.mdx @@ -2,15 +2,15 @@ title: "MCP Server" publishedAt: "2026-04-29" author: "Maximilian Kaske" -description: "Connect Claude, ChatGPT, Cursor, and any other Model Context Protocol client to your workspace — chat-shaped AI access with audit-logged mutations and explicit notify semantics." +description: "Connect Claude, ChatGPT, Cursor, and any other Model Context Protocol client to your workspace — sign in with OAuth, pick a workspace, and get audit-logged mutations with explicit notify semantics." category: "Product" faq: - question: "What does the MCP server let an assistant do?" answer: "The MCP server exposes 19 read and write tools scoped to your workspace: list status pages, monitors, response logs, notifications, private locations, and audit logs; inspect a single monitor or response log; create, update, and resolve status reports; schedule maintenance windows. Every mutation writes to the audit log, and publishing tools require an explicit notify decision." - question: "Are skills different from the MCP server?" answer: "Skills are prompt and command bundles installed into your local agent (Claude Code, etc.) that wrap the openstatus CLI. The MCP server is a remote endpoint that any MCP-compatible client can connect to. Skills are great for terminal-shaped workflows; MCP is great for chat-shaped workflows." - - question: "Do I need a separate MCP credential?" - answer: "No. We don't implement the MCP OAuth flow — there's no consent screen and no extra credential to manage. Authentication is the x-openstatus-key header, the same key the CLI, API, and Terraform use." + - question: "Do I need an API key to connect an MCP client?" + answer: "No. The MCP server is an OAuth 2.1 authorization server: Claude, ChatGPT, Cursor, and other clients open a consent screen where you pick a workspace and read-only or read & write access, and tokens are issued and refreshed automatically. The x-openstatus-key header still works for CI and headless agents, with the same key the CLI, API, and Terraform use." - question: "Can the assistant accidentally notify subscribers?" answer: "No. Every publishing tool requires the assistant to set notify: true | false explicitly — there's no default. Metadata-only edits like update_status_report can't notify at all. Every call lands in the audit log under actor_type = 'mcp', so you can see exactly what happened." --- @@ -19,21 +19,11 @@ The openstatus MCP server connects Claude Desktop, ChatGPT, Cursor, and any othe ## Quickstart -```json -{ - "mcpServers": { - "openstatus": { - "type": "http", - "url": "https://api.openstatus.dev/mcp", - "headers": { - "x-openstatus-key": "os_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - } - } - } -} +``` +https://api.openstatus.dev/mcp ``` -Drop the snippet into your client's MCP config (`claude_desktop_config.json`, Cursor settings, ChatGPT custom connector), restart, and the openstatus tools appear in the picker. The server is stateless Streamable HTTP and scales horizontally. +Add the URL as a custom connector in Claude, ChatGPT, or Cursor, or run `claude mcp add --transport http openstatus https://api.openstatus.dev/mcp` for Claude Code. The client opens a consent screen, you pick a workspace and an access level, and the openstatus tools appear in the picker. The server is stateless Streamable HTTP and scales horizontally. ## What you can do @@ -54,11 +44,13 @@ The same tool registry powers the [in-app assistant](/blog/from-clickops-to-chat ## Auth & scopes -Authentication is the `x-openstatus-key` header — the same API key the CLI, API, and Terraform use. We don't implement the MCP OAuth flow: no separate credential to manage, no consent screen, just one key from **Settings → API Tokens**. +The MCP server is an OAuth 2.1 authorization server. Connect from any client that implements the MCP authorization spec and you get a consent screen instead of a key to paste: pick the **workspace** and choose **Read-only** or **Read & write**. Tokens rotate automatically, and every connection is listed under **Settings > Integrations > Connected apps**, where it can be revoked with immediate effect. + +For CI and headless agents, the `x-openstatus-key` header still works — the same API key the CLI, API, and Terraform use, from **Settings > API Tokens**. -API keys carry a `read` or `write` scope. Read-only keys never see write tools in `tools/list` — they don't exist for that session. Write tools throw a clean error if a read-only key somehow attempts them. +Both credentials carry a `read` or `write` scope. Read-only connections never see write tools in `tools/list` — they don't exist for that session. Write tools throw a clean error if a read-only credential somehow attempts them. -Every mutation lands in the audit log under `actor_type = 'mcp'`, so you can trace any change back to a key, a user, and a transport. +Every mutation lands in the audit log under `actor_type = 'mcp'`, so you can trace any change back to a connection, a user, and a transport. ## Other surfaces @@ -67,9 +59,15 @@ Every mutation lands in the audit log under `actor_type = 'mcp'`, so you can tra - Building an in-app integration? Call the [API](/tooling/api) directly. - Want Slack instead of Claude or ChatGPT? See the [Slack agent](/status-page#slack-agent). +## Setup guides + +- [Claude Code](/guides/connect-openstatus-to-claude-code) · [Claude Desktop](/guides/connect-openstatus-to-claude-desktop) +- [Codex](/guides/connect-openstatus-to-codex) · [ChatGPT](/guides/connect-openstatus-to-chatgpt) +- [opencode](/guides/connect-openstatus-to-opencode) + ## Reference -Full tool schema, error codes, and per-client config in the [MCP reference](https://www.openstatus.dev/docs/reference/mcp-server/). +Full tool schema, error codes, and per-client config in the [MCP reference](/docs/reference/mcp-server/). ## Frequently asked questions @@ -85,9 +83,9 @@ Skills are prompt and command bundles installed into your local agent (Claude Co
    -
    +
    -No. We don't implement the MCP OAuth flow — there's no consent screen and no extra credential to manage. Authentication is the x-openstatus-key header, the same key the CLI, API, and Terraform use. +No. The MCP server is an OAuth 2.1 authorization server: Claude, ChatGPT, Cursor, and other clients open a consent screen where you pick a workspace and read-only or read & write access, and tokens are issued and refreshed automatically. The `x-openstatus-key` header still works for CI and headless agents, with the same key the CLI, API, and Terraform use.
    diff --git a/apps/web/src/content/pages/product/tooling/terraform.mdx b/apps/web/src/content/pages/product/tooling/terraform.mdx index 410a8dcf8..ca47adfa3 100644 --- a/apps/web/src/content/pages/product/tooling/terraform.mdx +++ b/apps/web/src/content/pages/product/tooling/terraform.mdx @@ -12,7 +12,7 @@ faq: - question: "How do I migrate an existing workspace into Terraform?" answer: "Run openstatus terraform generate from the CLI. It fetches your full configuration and produces valid HCL with cross-references and import blocks — no hand-writing required." - question: "Where do I get an API token?" - answer: "Generate one from Settings → API Tokens, then set OPENSTATUS_API_TOKEN before running terraform plan. The same token works across the CLI, API, and MCP server." + answer: "Generate one from Settings > API Tokens, then set OPENSTATUS_API_TOKEN before running terraform plan. The same token works across the CLI, API, and MCP server." --- The [openstatus Terraform provider](https://registry.terraform.io/providers/openstatusHQ/openstatus/latest) treats monitoring as code: monitors, notifications, and status pages live in the same plan-and-apply lifecycle as the rest of your infrastructure. @@ -62,7 +62,7 @@ The output is a valid HCL configuration with cross-references between resources ## Auth & audit -Set `OPENSTATUS_API_TOKEN` before running Terraform. The token is the same one the CLI, API, and MCP server share — generate it from **Settings → API Tokens**. +Set `OPENSTATUS_API_TOKEN` before running Terraform. The token is the same one the CLI, API, and MCP server share — generate it from **Settings > API Tokens**. Every mutation through the provider lands in the audit log under `actor_type = 'api'`, so you can trace any change back to a key, a user, and a transport. @@ -98,6 +98,6 @@ Run openstatus terraform generate from the CLI. It fetches your full configurati
    -Generate one from Settings → API Tokens, then set `OPENSTATUS_API_TOKEN` before running terraform plan. The same token works across the CLI, API, and MCP server. +Generate one from Settings > API Tokens, then set `OPENSTATUS_API_TOKEN` before running terraform plan. The same token works across the CLI, API, and MCP server.
    diff --git a/apps/web/src/content/pages/use-case/agent.mdx b/apps/web/src/content/pages/use-case/agent.mdx index 7150c5b2d..875ec0e3c 100644 --- a/apps/web/src/content/pages/use-case/agent.mdx +++ b/apps/web/src/content/pages/use-case/agent.mdx @@ -6,7 +6,7 @@ description: "Let an AI agent run your status page. It receives openstatus webho category: "AI Agents" faq: - question: "What is the openstatus MCP server?" - answer: "Openstatus ships a remote MCP (Model Context Protocol) server at api.openstatus.dev/mcp. It exposes typed tools for every workspace entity — monitors, response logs, status pages, page components, status reports, maintenance windows, notification channels, and audit logs. Any MCP-compatible client (Claude Desktop, ChatGPT, Cursor) or any agent built with the MCP SDK can connect with an openstatus API key." + answer: "Openstatus ships a remote MCP (Model Context Protocol) server at api.openstatus.dev/mcp. It exposes typed tools for every workspace entity — monitors, response logs, status pages, page components, status reports, maintenance windows, notification channels, and audit logs. Any MCP-compatible client (Claude Desktop, ChatGPT, Cursor) or any agent built with the MCP SDK can connect: interactive clients sign in with OAuth and pick a workspace and access level on a consent screen, headless agents pass an openstatus API key." - question: "How does an agent know a monitor failed?" answer: "Configure a webhook notification channel on the monitor. When openstatus detects a failure, it posts a payload to your endpoint. Your agent runtime receives it, opens an MCP session against api.openstatus.dev/mcp using your workspace API key, and starts investigating with the tools below." - question: "Which MCP tools matter for incident response?" @@ -162,7 +162,7 @@ Every status-report mutation requires the agent to choose whether to notify subs
    -Openstatus ships a remote MCP (Model Context Protocol) server at api.openstatus.dev/mcp. It exposes typed tools for every workspace entity — monitors, response logs, status pages, page components, status reports, maintenance windows, notification channels, and audit logs. Any MCP-compatible client (Claude Desktop, ChatGPT, Cursor) or any agent built with the MCP SDK can connect with an openstatus API key. +Openstatus ships a remote MCP (Model Context Protocol) server at api.openstatus.dev/mcp. It exposes typed tools for every workspace entity — monitors, response logs, status pages, page components, status reports, maintenance windows, notification channels, and audit logs. Any MCP-compatible client (Claude Desktop, ChatGPT, Cursor) or any agent built with the MCP SDK can connect: interactive clients sign in with OAuth and pick a workspace and access level on a consent screen, headless agents pass an openstatus API key.