> Every Sentinel doc, as Markdown: https://runsentinel.app/llms.txt

# MCP server

A hosted MCP server for Stacks contract monitoring: call show, when and logs from Claude Code, Codex or Cursor. Use the hosted server, or run it locally.

## Hosted

Stateless: each request is signed in on its own, and nothing is kept between calls. Sign in with your browser, no key needed.

Claude Code:

```sh
claude mcp add --transport http sentinel https://api.runsentinel.app/mcp
```

Then run `/mcp`, pick `sentinel` and sign in. The browser opens runsentinel.app: sign in with your email link, check the app name and allow it. You can lower each area it asks for. It shows under Settings → API keys as an app; revoke it there.

Cursor, in `~/.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "sentinel": { "url": "https://api.runsentinel.app/mcp" }
  }
}
```

For CI or a headless agent, send a key instead (Settings → API keys or `sentinel keys create`). It looks like `sk-snt_…`; a secondlayer API key won't work here.

```sh
claude mcp add --transport http sentinel https://api.runsentinel.app/mcp \
  --header "Authorization: Bearer sk-snt_…"
```

The endpoint is `POST /mcp` (Streamable HTTP). No bearer gets `401` with `WWW-Authenticate: Bearer resource_metadata=…`, which starts the sign-in (OAuth 2.1 with PKCE, client registration by DCR). `GET` and `DELETE` get `405`. Each tool call runs through the API with that sign-in's areas, and the 120 requests a minute apply. An app's access token works only on `/mcp`: it lasts an hour and refreshes for 30 days.

## Local

Over stdio, Node 22 or newer.

```sh
claude mcp add sentinel \
  --env SENTINEL_API_KEY=sk-snt_… \
  -- npx -y -p @secondlayer/sentinel sentinel-mcp
```

```json
{
  "mcpServers": {
    "sentinel": {
      "command": "npx",
      "args": ["-y", "-p", "@secondlayer/sentinel", "sentinel-mcp"],
      "env": { "SENTINEL_API_KEY": "sk-snt_…" }
    }
  }
}
```

Without `SENTINEL_API_KEY`, the local server uses the login `sentinel login` saved. `SENTINEL_API_URL` is optional.

### First run

No login yet? The server still starts. The first tool call returns `sign_in_required` with a link and a code (`structuredContent: { status, verificationUrl, userCode, expiresAt }`). Open the link and confirm the code, then call the tool again: it waits up to 60 s for the approval, saves the login and runs.

## Let an agent run it

{/* permissions:start */}
Let an agent run the reads without asking. Everything else (`when`, `every`, `at`, `off`, `destinations add`, `remove` …) stays a prompt, so a person sees the full line before anything is added, rerouted or stopped. The read verbs refuse `--yes` and every write flag, so appending flags to an allowed prefix can't widen it. Don't add a blanket `sentinel *` rule, and don't list `keys`.

Claude Code, in `.claude/settings.json`. An `ask` rule beats an `allow` rule, so there is none: Claude Code asks for any command that isn't allowed. The `mcp__sentinel__` rules assume the server is named `sentinel`.

```json
{
  "permissions": {
    "allow": [
      "Bash(sentinel whoami *)",
      "Bash(sentinel status *)",
      "Bash(sentinel alerts *)",
      "Bash(sentinel ask *)",
      "Bash(sentinel contracts *)",
      "Bash(sentinel describe *)",
      "Bash(sentinel list *)",
      "Bash(sentinel show *)",
      "Bash(sentinel logs *)",
      "Bash(sentinel destinations *)",
      "mcp__sentinel__whoami",
      "mcp__sentinel__status",
      "mcp__sentinel__alerts",
      "mcp__sentinel__ask",
      "mcp__sentinel__contracts",
      "mcp__sentinel__describe",
      "mcp__sentinel__list",
      "mcp__sentinel__show",
      "mcp__sentinel__logs",
      "mcp__sentinel__destinations"
    ]
  }
}
```

OpenCode, in `opencode.json`. The last matching rule wins, so the catch-all comes first.

```json
{
  "$schema": "https://opencode.ai/config.json",
  "permission": {
    "bash": {
      "sentinel *": "ask",
      "sentinel whoami": "allow",
      "sentinel whoami *": "allow",
      "sentinel status": "allow",
      "sentinel status *": "allow",
      "sentinel alerts": "allow",
      "sentinel alerts *": "allow",
      "sentinel ask": "allow",
      "sentinel ask *": "allow",
      "sentinel contracts": "allow",
      "sentinel contracts *": "allow",
      "sentinel describe": "allow",
      "sentinel describe *": "allow",
      "sentinel list": "allow",
      "sentinel list *": "allow",
      "sentinel show": "allow",
      "sentinel show *": "allow",
      "sentinel logs": "allow",
      "sentinel logs *": "allow",
      "sentinel destinations": "allow",
      "sentinel destinations *": "allow"
    }
  }
}
```
{/* permissions:end */}

The agent skill (`npx skills add https://runsentinel.app`) teaches the same rules; see [Quickstart](/docs/quickstart).

## Confirm before it happens

The tools are the CLI's verbs, one each (`destinations add` is `destinations_add`), and they answer the same way: a write returns `{ done, willDo, effect, next }`, a read its data and `next`. `next` is the next command, `sentinel <verb> …`; as a call, its verb is the tool.

- **Read** tools never write.
- **Write** tools act at once, and `dry_run: true` previews any of them. `when`, `every` and `at` answer a cost (a check, a contract over your tier's count) with `done: false` until `confirm: true`.
- **Confirm** tools (`off`, `remove`, `destinations_remove`, `backtest`, `test_alert`) return a preview (`done: false`) and change nothing until you call them again with `confirm: true`.

> **Starts plans**
>
> `show` (on a contract), `import`, `update` and `plan_linked` start plans, which are free. A key with Plans write can start them. Give an agent that area only on purpose.

## Tools

A `target` is a contract (its id, or its name on your account), or an id: a watch `w_…`, a schedule `sch_…`, a check run `chk_…`, an alert `alr_…`.

{/* mcp-tools:start */}
### Read

| Tool | Input | Does | Needs |
|---|---|---|---|
| `describe` | `text` | A sentence or line as one watch: its line, 90-day backtest, cost. Writes nothing. | Monitoring: write |
| `list` | `target?`, `all?` | Your watches and schedules by id; all: true adds suggestions. | Monitoring: read |
| `show` | `id`, `series?`, `limit?` | A watch (w_…), schedule (sch_…), check run (chk_…), alert (alr_…), or a contract: what its code has (starts its free plan). sbtc\|pox5 + series reserve\|custody\|stbtc. | Plans: write; Monitoring: read; Alerts: read |
| `logs` | `target?`, `only?`, `since?`, `tx?`, `after?`, `limit?` | Events, alerts, checks, schedule runs and changes, oldest first. Pass nextCursor as after. | Monitoring: read |
| `alerts` | `contract?`, `after?`, `before?`, `watchId?` | Your alerts, newest first; after (alert id or ISO time): the ones since. | Alerts: read |
| `contracts` | none | Your contracts: added, or only watched. | Plans: read |
| `whoami` | none | Your account, tier, live contracts against its limit and the period's check usage. | any key |
| `status` | none | Is monitoring up, measured; yours too. | any key |
| `ask` | `question` | Ask about Sentinel itself, from its docs. | any key |
| `destinations` | none | Your alert destinations (what --to picks); defaults get every watch with no --to. | Monitoring: read |

### Write: at once, `dry_run` previews

| Tool | Input | Does | Needs |
|---|---|---|---|
| `upgrade` | `tier?`, `id?`, `dry_run?` | A Stripe link to subscribe (tier: builder\|team), or to the portal once subscribed. Show the link to the person; never open or pay it. After they say they're done, pass its id for the status. | any key |
| `mute` | `target`, `duration?`, `dry_run?` | Mute a watch (w_ id) or an alert's watch; off unmutes. | Monitoring: write; Alerts: write |
| `status_watch` | `on`, `dry_run?` | A monitoring-is-up line in your digests: on or off. | Monitoring: write |
| `read` | `alertId?`, `all?`, `dry_run?` | Mark an alert read, or all. | Alerts: write |
| `rename` | `target`, `name`, `dry_run?` | Your name for a contract ("" clears it). | Plans: write |
| `retry_history` | `target`, `dry_run?` | Read a contract's history again after a failed read. | Monitoring: write |
| `redeliver` | `alertId`, `dry_run?` | Resend an alert where it failed. | Alerts: write |
| `on` | `target`, `deployer?`, `dry_run?` | Turn on a watch, a held schedule or a contract's monitoring. Billed once live. deployer arms pre-launch. | Monitoring: write |
| `when` | `line`, `confirm?`, `dry_run?`, `wait?` | Add/edit watch: call &lt;target> &lt;fn\|@admin\|@value\|@control> [--caller X] [--new-caller] \| transfer &lt;target> &lt;stx\|token\|*> --in\|--out [--over N\|20%] [--within 1h] \| mint &lt;token> &lt;name> [--new-minter\|--over N] \| print &lt;target> &lt;topic\|*> \| state &lt;target> &lt;read> [--drops 20% --within 1h\|--crosses N] \| deploy &lt;address> \| sbtc &lt;stall\|payout\|mint\|rotation\|protocol-update\|reserve\|peg> [--gap N] [--open N] [--after 6h] \| pox5 &lt;bond-setup\|admin\|early-exit>; [--to &lt;ref>] [--note "…"] [--check "&lt;q>"] [--hold]. wait ≤60s. | Monitoring: write |
| `every` | `line`, `confirm?`, `dry_run?` | On a clock: &lt;6h\|1d\|monday 09:00\|cron> [&lt;target>] --digest [--ask "&lt;q>"] \| --check "&lt;q>" [--tz Z] [--to &lt;ref>]. No target: all your watches. | Monitoring: write |
| `at` | `line`, `confirm?`, `dry_run?` | Once: &lt;iso> [&lt;target>] --digest \| --check "&lt;q>" [--to &lt;ref>]. | Monitoring: write |
| `import` | `url`, `only?`, `all?`, `commit?`, `deployer?`, `hold?`, `dry_run?` | A public Clarinet repo onto your account (free plans). deployer arms each for its deploy; hold keeps watches off. | Plans: write |
| `group` | `target`, `group?`, `add?`, `remove?`, `dry_run?` | A contract's group members and their source; group + add/remove corrects them for you. | Monitoring: write |
| `destinations_add` | `kind`, `target`, `label?`, `default?`, `confirm?`, `dry_run?` | Add a Slack #channel, Slack webhook or signed webhook (label), or email. default: watches with no --to go there. Another person's email needs confirm. | Monitoring: write |
| `destinations_default` | `ref`, `on`, `dry_run?` | Make a destination (ref or d_…) a default, or not. | Monitoring: write |
| `update` | `target`, `commit?`, `dry_run?` | Plan a pre-launch contract's latest commit (or commit); the preview names its branch. | Plans: write |
| `auto_plan` | `target`, `on`, `dry_run?` | Re-plan a waiting plan on its own once its contracts deploy. | Plans: write |
| `link` | `target`, `contract`, `url`, `dry_run?` | Link the code of a contract a waiting plan needs; url null clears. | Plans: write |
| `plan_linked` | `target`, `dry_run?` | Plan a waiting contract now with its linked code. | Plans: write |
| `connect_slack` | `status?`, `remove?`, `confirm?`, `dry_run?` | The Slack app: an "Add to Slack" link; status lists workspaces; remove disconnects one. | Monitoring: write |

### Confirm: previews until `confirm`

| Tool | Input | Does | Needs |
|---|---|---|---|
| `test_alert` | `target`, `example?`, `confirm?`, `dry_run?` | The sample alert; confirm sends it where the contract's alerts go. | Monitoring: write |
| `backtest` | `id`, `run?`, `confirm?`, `dry_run?` | Run a watch's check on its last 1-5 events now: paid, never delivered. | Monitoring: write |
| `off` | `target`, `confirm?`, `dry_run?` | Pause a watch or a contract (nothing deleted; on resumes). A schedule id stops it. | Monitoring: write |
| `remove` | `target`, `confirm?`, `dry_run?` | Take a contract off your account. Can't be undone. | Plans: write |
| `destinations_remove` | `ref`, `confirm?`, `dry_run?` | Remove a destination (ref or d_…). | Monitoring: write |
{/* mcp-tools:end */}

## Limits

A key without the area a tool needs gets the API's `403 This key needs Monitoring: write.` back from the tool. Sign-in, billing and API keys stay on the account page (`/account`); everything else, a tool does.
