# MCP Contract

## Scope

Runtime MCP contract for the hosted service and local development. The MCP server calls the same domain/API rules as the core service. This document defines resources, tools, prompts, schemas, and auth boundaries; it does not authorize deployments or GitHub automation.

## Transport And Auth

- Streamable HTTP JSON-RPC transport at `https://mcp.niceuptime.com/mcp`.
- Local instance `N` uses `http://localhost:<18081 + N*100>/mcp`; Manual instance `0` is `http://localhost:18081/mcp`.
- Tenant-scoped bearer token required for every MCP request.
- Hosted production bearer tokens are rows in `mcp_access_tokens`, created from the authenticated owner settings flow and stored only as hashes.
- Static MCP environment tokens are local/debug only and are not the hosted production tenancy contract.
- Workspace owners create short-lived `read` or `write` tokens in Settings, receive the raw `nu_mcp_` secret once, and must store it in a trusted client or password manager.
- Read tokens can call read tools, resources, and prompts only.
- Write tokens can call every read surface and write tools, subject to each tool's product entitlement and live membership rule.
- Read scope never authorizes a write tool.
- Every role-sensitive maintenance-window or Incident review operation resolves the token creator's current tenant membership on each request.
- A demoted or removed token creator immediately loses the operation that requires the former role.
- Local static principals without an accountable user may use only eligible reads and cannot use an owner/admin operation.
- Pulse heartbeat tokens are rejected as MCP credentials.
- Runner bearer tokens are rejected as MCP credentials.
- Resource and tool handlers must scope every read and write by tenant id from auth context.

## Health And Readiness

- `GET /up` is liveness and does not replace readiness.
- `GET /healthz` returns service metadata, the database-backed auth model, and tenant-awareness metadata.
- `GET /readyz` is required production launch acceptance.
- `/readyz` must return ready only when auth config is safe, database connectivity works, the `mcp_access_tokens` table exists, and the service is tenant-aware.

## Method Surface

MCP standard methods exposed:

- `initialize`
- `resources/list`
- `resources/read`
- `tools/list`
- `tools/call`
- `prompts/list`
- `prompts/get`

Clients send `initialize`, then `notifications/initialized` before normal discovery and tool calls.
The server accepts `notifications/cancelled` for in-flight request cancellation notifications.
Malformed, oversized (over 1 MiB), unsupported, and under-scoped requests return JSON-RPC errors without exposing tenant or credential data.
Requests run inside the HTTP server's bounded read, header, and graceful-shutdown lifecycle.

## Resources

### `niceuptime://tenant/summary`

Tenant summary with active monitor counts, billing plan, current status totals, and open Incident count. Omits secrets and full Pulse tokens.

### `niceuptime://monitors/{monitor_id}`

Monitor detail for one tenant-owned Pulse or Probe monitor, including policy, lifecycle, current state, last Heartbeat summary, and Probe canonical target metadata when applicable.

### `niceuptime://incidents/{incident_id}`

Incident timeline for one tenant-owned Incident, including state transitions and Heartbeat references.

### `niceuptime://monitors/{monitor_id}/heartbeats/recent`

Recent Heartbeat feed for one monitor. Includes source (`pulse` or `probe`), status, observed time, region, latency, and capped detail. Does not reveal Pulse tokens.

## Tools

### `monitors.list`

Read tool. Lists tenant monitors.
Each monitor uses the same monitor payload shape as `monitors.read`: policy, regions, timestamps, and `probe` when a Probe subscription exists.
Monitors without regions return `"regions": []`; monitors without Probe config omit `probe`.
`status` is the canonical Heartbeat-derived state snapshot and is `unknown` until a snapshot exists.

Input:

```json
{
  "type": "pulse",
  "state": "active",
  "limit": 50
}
```

Output:

```json
{
  "monitors": [
    {
      "id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f4",
      "type": "probe",
      "name": "Public API",
      "state": "active",
      "status": "ok",
      "policy": {
        "expected_interval_seconds": 60,
        "grace_seconds": 30,
        "failure_threshold": 1,
        "recovery_threshold": 1
      },
      "regions": ["us-1", "eu-1"],
      "probe": {
        "method": "GET",
        "url": "https://example.com/health",
        "timeout_ms": 10000,
        "expected_status": {"mode": "class_2xx"},
        "tls_policy": "default",
        "headers": []
      },
      "created_at": "2026-07-09T12:00:00Z",
      "updated_at": "2026-07-09T12:00:00Z"
    }
  ]
}
```

### `monitors.read`

Read tool. Reads one tenant monitor by id.

Input:

```json
{
  "monitor_id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f4"
}
```

### `incidents.list`

Read tool. Lists tenant Incidents.

Input:

```json
{
  "state": "open",
  "limit": 50
}
```

### `incidents.read`

Read tool. Reads one tenant Incident timeline.

Input:

```json
{
  "incident_id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f5"
}
```

### `heartbeats.recent`

Read tool. Lists recent Heartbeats for one monitor.

Input:

```json
{
  "monitor_id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f4",
  "limit": 25
}
```

### `probe_targets.search`

Read tool. Searches shared Probe target metadata visible through tenant-owned Probe monitors. It never exposes other tenants.

Input:

```json
{
  "query": "example.com",
  "region": "us-1",
  "limit": 20
}
```

Output:

```json
{
  "targets": [
    {
      "fingerprint": "efd948f7c8090b60f7b44b517b05e187193956674bfc3e640578ef72df464723",
      "method": "GET",
      "url": "https://example.com/health",
      "regions": ["us-1"],
      "subscriber_count_visible": 1
    }
  ]
}
```

### `monitors.create_pulse`

Write tool. Creates a Pulse monitor and returns one generated integration snippet. The full token appears only in this creation response.

Input:

```json
{
  "name": "Nightly invoice job",
  "policy": {
    "expected_interval_seconds": 86400,
    "grace_seconds": 900,
    "failure_threshold": 1,
    "recovery_threshold": 1
  }
}
```

Output:

```json
{
  "monitor_id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f4",
  "snippet": "curl -X POST https://api.example.test/v1/heartbeats/REDACTED_ON_READ -H 'content-type: application/json' -d '{\"status\":\"success\",\"observed_at\":\"2026-06-17T14:00:00Z\"}'"
}
```

### `monitors.create_probe`

Write tool. Creates a Probe monitor subscription to a canonical target. It uses shared runner execution and produces synthetic Heartbeats through the API batch path.
Within the create transaction, each canonicalized region upserts the canonical target's shared lease schedule at the monitor interval.
A matching schedule keeps its earliest due time.
The tool canonicalizes accepted Railway region aliases to stable Probe region ids and deduplicates them before entitlement counting and persistence.
The write is atomic: if any step fails, every write in the create transaction rolls back.

Input:

```json
{
  "name": "Public homepage",
  "regions": ["us-1"],
  "policy": {
    "expected_interval_seconds": 60,
    "grace_seconds": 30,
    "failure_threshold": 2,
    "recovery_threshold": 2
  },
  "probe": {
    "method": "GET",
    "url": "https://example.com/",
    "timeout_ms": 5000,
    "expected_status": {
      "mode": "class_2xx"
    },
    "tls_policy": "require_valid",
    "headers": [],
    "assertions": [
      {"kind": "body_contains", "value": "healthy"},
      {"kind": "json_pointer_equals", "pointer": "/data/status", "value": "healthy"}
    ]
  }
}
```

`assertions` defaults to `[]`.
Only Max tenants can create a Probe with non-empty `assertions`.
At most 10 unique assertions are accepted.
`body_contains` requires `value`.
`json_pointer_equals` requires an RFC 6901 `pointer` and `value`.
`HEAD` Probes cannot use assertions.

### `maintenance_windows.list`

Read tool.
Lists the same keyset-paginated maintenance-window summary, selectable monitors, selectable status pages, and `can_manage` value returned by `GET /v1/maintenance-windows`.
Normal listing requires a Max tenant.
`cancelable_only: true` lists only scheduled or active windows and requires a current owner or admin, but remains available to that actor for downgrade cleanup after the tenant leaves Max.

Input:

```json
{
  "cancelable_only": false,
  "limit": 100,
  "cursor": "opaque-maintenance-window-cursor"
}
```

`cancelable_only` defaults to `false`.
`limit` defaults to `100` and must be from `1` through `200`.
`cursor` is the opaque `next_cursor` value from the preceding result.

Output:

```json
{
  "windows": [
    {
      "id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f4",
      "display_id": "MW-018f6cc2",
      "title": "Database maintenance",
      "timezone": "America/New_York",
      "starts_at": "2026-08-10T02:00:00Z",
      "ends_at": "2026-08-10T04:00:00Z",
      "recurrence": "none",
      "status": "scheduled",
      "monitors": [{"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f5", "name": "Public API"}]
    }
  ],
  "monitors": [{"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f5", "name": "Public API"}],
  "status_pages": [{"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f6", "name": "Public status"}],
  "can_manage": true,
  "next_cursor": "opaque-maintenance-window-cursor"
}
```

`next_cursor` is omitted on the final page.

### `maintenance_windows.read`

Read tool.
Reads the same tenant-scoped maintenance-window payload as `GET /v1/maintenance-windows/{maintenance_window_id}`.
It requires a Max tenant.

Input:

```json
{
  "maintenance_window_id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f4"
}
```

Output:

```json
{
  "maintenance_window": {
    "id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f4",
    "display_id": "MW-018f6cc2",
    "title": "Database maintenance",
    "description": "Routine database maintenance.",
    "timezone": "America/New_York",
    "starts_at": "2026-08-10T02:00:00Z",
    "ends_at": "2026-08-10T04:00:00Z",
    "recurrence": "none",
    "expected_impact": "minor",
    "notify_subscribers": true,
    "reminder_minutes": [60, 15],
    "status_page_message": "We will return shortly.",
    "status": "scheduled",
    "created_at": "2026-08-08T12:00:00Z",
    "created_by": {"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f7", "name": "Workspace owner"},
    "monitors": [{"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f5", "name": "Public API"}],
    "status_pages": [{"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f6", "name": "Public status"}],
    "events": [{"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f8", "event_type": "scheduled", "message": "Maintenance window scheduled", "happened_at": "2026-08-08T12:00:00Z", "actor_name": "Workspace owner"}],
    "subscriber_count": 42,
    "can_manage": true
  }
}
```

`activated_at`, `ended_at`, `cancelled_at`, `created_by`, and an event's `actor_name` are omitted when unavailable.
`monitors`, `status_pages`, `events`, and `reminder_minutes` are arrays and return `[]` when empty.

### `maintenance_windows.create`

Write tool.
Max-plan owners and admins create a maintenance window under the existing idempotency, target validation, audit, reminder, and subscriber-delivery transaction.
The output is exactly the `maintenance_window` payload from `maintenance_windows.read`.

Input:

```json
{
  "idempotency_key": "schedule-maintenance-2026-08-10",
  "title": "Database maintenance",
  "description": "Routine database maintenance.",
  "starts_at_local": "2026-08-09T22:00",
  "ends_at_local": "2026-08-10T00:00",
  "timezone": "America/New_York",
  "recurrence": "none",
  "expected_impact": "minor",
  "monitor_ids": ["018f6cc2-6f89-7c2b-9f75-17f47f5787f5"],
  "status_page_ids": ["018f6cc2-6f89-7c2b-9f75-17f47f5787f6"],
  "notify_subscribers": true,
  "reminder_minutes": [15, 60],
  "status_page_message": "We will return shortly."
}
```

`idempotency_key` is required, is trimmed, and must contain from `1` through `120` characters.
`title` is required and is at most `120` characters.
`description` is at most `2000` characters.
`starts_at_local` and `ends_at_local` use `YYYY-MM-DDTHH:MM`, resolve unambiguously in the supplied IANA `timezone`, start in the future, and span no more than 30 days.
`recurrence` is one of `none`, `daily`, `weekly`, or `monthly`.
`expected_impact` is one of `none`, `minor`, `partial`, or `major`.
`monitor_ids` is a required non-empty unique UUID list.
`status_page_ids` is an optional unique UUID list.
`notify_subscribers: true` requires at least one status-page ID.
`reminder_minutes` values range from `1` through `10080`, are deduplicated and sorted descending, and are cleared when `notify_subscribers` is false.
Each of `monitor_ids`, `status_page_ids`, and `reminder_minutes` accepts at most `100` values.
`status_page_message` is at most `500` characters.

Output:

```json
{
  "maintenance_window": {"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f4"}
}
```

The abbreviated output above has every field defined by the `maintenance_windows.read` output.
Reusing a completed create key returns that existing maintenance window.

### `maintenance_windows.update`

Write tool.
Max-plan owners and admins replace a scheduled maintenance window using the same fields and validation as `maintenance_windows.create`.
Only scheduled windows are editable.

Input:

```json
{
  "maintenance_window_id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f4",
  "idempotency_key": "update-maintenance-2026-08-10",
  "title": "Database maintenance",
  "description": "Routine database maintenance.",
  "starts_at_local": "2026-08-09T22:00",
  "ends_at_local": "2026-08-10T00:00",
  "timezone": "America/New_York",
  "recurrence": "none",
  "expected_impact": "minor",
  "monitor_ids": ["018f6cc2-6f89-7c2b-9f75-17f47f5787f5"],
  "status_page_ids": [],
  "notify_subscribers": false,
  "reminder_minutes": [],
  "status_page_message": ""
}
```

`maintenance_window_id` is a required UUID.
`idempotency_key` and every remaining field follow the `maintenance_windows.create` contract.

Output:

```json
{
  "maintenance_window": {"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f4"}
}
```

The output has every field defined by the `maintenance_windows.read` output.

### `maintenance_windows.post_update`

Write tool.
Max-plan owners and admins post an idempotent update only while a maintenance window is scheduled or active.
The existing transaction writes the event, audit record, and subscriber delivery when notifications are enabled.

Input:

```json
{
  "maintenance_window_id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f4",
  "idempotency_key": "maintenance-progress-1",
  "message": "The migration is underway."
}
```

`maintenance_window_id` is a required UUID.
`idempotency_key` is required and follows the `maintenance_windows.create` contract.
`message` is required after trimming and is at most `1000` characters.

Output:

```json
{
  "maintenance_window": {"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f4"}
}
```

The output has every field defined by the `maintenance_windows.read` output and includes the event timeline after the post.

### `maintenance_windows.cancel`

Write tool.
Current owners and admins may cancel a scheduled or active maintenance window, including the safe downgrade-cleanup action after the tenant leaves Max.
Completed windows cannot be cancelled.
Cancelling an already cancelled window succeeds and returns its existing payload.

Input:

```json
{
  "maintenance_window_id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f4",
  "idempotency_key": "cancel-maintenance-2026-08-10"
}
```

`maintenance_window_id` is required and is a UUID.
`idempotency_key` is optional, is at most `120` characters, and defaults to the stable `cancel` key when omitted.

Output:

```json
{
  "maintenance_window": {"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f4", "status": "cancelled"}
}
```

The output has every field defined by the `maintenance_windows.read` output.

### `incident_reviews.read`

Read tool.
Reads the canonical Incident review stored by the existing postmortem lifecycle.
It requires a Max tenant.
Static tokens without a user may read eligible tenant data, while user-bound tokens must still resolve to a current workspace membership.

Input:

```json
{
  "incident_id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f9"
}
```

Output:

```json
{
  "incident_review": {
    "id": "018f6cc2-6f89-7c2b-9f75-17f47f5787fa",
    "incident_id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f9",
    "title": "Database availability incident",
    "summary": "A maintenance migration delayed requests.",
    "impact": "Some requests were delayed.",
    "root_cause": "The migration exceeded its planned duration.",
    "resolution": "The migration was completed and capacity restored.",
    "visibility": "internal",
    "state": "draft",
    "version": 1,
    "authors": [{"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f7", "first_name": "Workspace", "last_name": "Owner"}],
    "action_items": [{"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787fb", "description": "Add migration timing alerts.", "completed": false, "position": 0, "assignee": null}],
    "created_by": {"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f7", "first_name": "Workspace", "last_name": "Owner"},
    "last_edited_by": {"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f7", "first_name": "Workspace", "last_name": "Owner"},
    "published_at": null,
    "created_at": "2026-08-08T12:00:00Z",
    "updated_at": "2026-08-08T12:00:00Z",
    "can_manage": true
  }
}
```

### `incident_reviews.create`

Write tool.
Max-plan owners and admins create one canonical Incident review draft for a tenant-owned Incident, or return the existing canonical draft.

Input:

```json
{
  "incident_id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f9"
}
```

Output:

```json
{
  "incident_review": {"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787fa"}
}
```

The output has every field defined by the `incident_reviews.read` output.

### `incident_reviews.save`

Write tool.
Max-plan owners and admins replace the canonical Incident review, its ordered authors, and its ordered action items with optimistic version checking.

Input:

```json
{
  "incident_id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f9",
  "version": 1,
  "title": "Database availability incident",
  "summary": "A maintenance migration delayed requests.",
  "impact": "Some requests were delayed.",
  "root_cause": "The migration exceeded its planned duration.",
  "resolution": "The migration was completed and capacity restored.",
  "visibility": "internal",
  "author_ids": ["018f6cc2-6f89-7c2b-9f75-17f47f5787f7"],
  "action_items": [
    {
      "id": "",
      "description": "Add migration timing alerts.",
      "completed": false,
      "assignee_user_id": null
    }
  ]
}
```

`incident_id` is a required UUID.
`version` is a required positive integer and must match the current review version.
`title` is required after trimming and is from `1` through `200` characters.
`summary`, `impact`, `root_cause`, and `resolution` each permit safe text through `20000` characters.
`visibility` is `internal` or `public`.
`author_ids` is a required unique list of `1` through `25` current-workspace-user UUIDs.
`action_items` has at most `100` items and preserves input order.
Each action has an existing UUID or an empty `id` for a new item, a required `description` from `1` through `1000` characters, `completed`, and a UUID or `null` `assignee_user_id`.

Output:

```json
{
  "incident_review": {"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787fa", "version": 2}
}
```

The output has every field defined by the `incident_reviews.read` output.

### `incident_reviews.publish`

Write tool.
Max-plan owners and admins publish a complete review for a resolved Incident with optimistic version checking and an explicit visibility confirmation.

Input:

```json
{
  "incident_id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f9",
  "version": 2,
  "visibility_confirmed": true
}
```

`incident_id` is a required UUID.
`version` is a required positive integer and must match the current review version.
`visibility_confirmed` is required and must be `true`.

Output:

```json
{
  "incident_review": {"id": "018f6cc2-6f89-7c2b-9f75-17f47f5787fa", "state": "published"}
}
```

The output has every field defined by the `incident_reviews.read` output.

### `incident_reviews.unpublish`

Write tool.
Current owners and admins may remove a published Incident review from public or internal publication, including the safe downgrade-cleanup action after the tenant leaves Max.

Input:

```json
{
  "incident_id": "018f6cc2-6f89-7c2b-9f75-17f47f5787f9",
  "version": 2
}
```

`incident_id` is required and is a UUID.
`version` is optional and, when present, is a positive integer.
Max callers use the current version.
Downgraded owners and admins may omit the version for cleanup.

Output:

```json
{}
```

The successful empty result matches the REST unpublish operation's `204 No Content` response.

## Tool Errors

Common error codes:

- `unauthorized`: missing or invalid MCP token.
- `forbidden_scope`: token lacks read or write scope.
- `forbidden`: the current actor lacks the required workspace membership or owner/admin role.
- `not_found`: object absent or outside tenant scope.
- `paywall_required`: product entitlement is unavailable for the requested operation.
- `validation_failed`: input schema rejected.
- `conflict`: lifecycle, idempotency, or optimistic-version conflict.

## Prompts

### `incident.summary`

Inputs: `incident_id`, optional `tone`.

Output constraints:

- summarize tenant-owned Incident timeline only.
- include Pulse or Probe source labels where visible.
- do not infer data from other tenants.
- do not reveal hidden tokens.

### `pulse.integration_snippet`

Inputs: `monitor_id`, `shell`.

Output constraints:

- generate snippet for `POST /v1/heartbeats/{token}` only when caller has write scope or token reveal permission.
- otherwise return redacted token and instructions to rotate/reveal in UI/API.
- keep payload fields aligned with `contracts/heartbeat.schema.json`.

### `probe.failure_explainer`

Inputs: `monitor_id`, optional `heartbeat_id`.

Output constraints:

- explain Probe failure using recent source=`probe` Heartbeats, status policy, region, timings, and capped detail.
- state that API derives Incidents from Heartbeats and tenant policy.
- avoid deployment or infrastructure provisioning instructions.

## Acceptance Notes

- `tools/list` must include `monitors.list`, `monitors.read`, `incidents.list`, `incidents.read`, `heartbeats.recent`, `probe_targets.search`, `monitors.create_pulse`, `monitors.create_probe`, `maintenance_windows.list`, `maintenance_windows.read`, `maintenance_windows.create`, `maintenance_windows.update`, `maintenance_windows.post_update`, `maintenance_windows.cancel`, `incident_reviews.read`, `incident_reviews.create`, `incident_reviews.save`, `incident_reviews.publish`, and `incident_reviews.unpublish`.
- `resources/list` must include all resource URI templates above.
- `prompts/list` must include all prompts above.
- All writes reuse API product entitlements and validation rules.
