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.
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:
claude mcp add --transport http sentinel https://api.runsentinel.app/mcpThen 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:
{
"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.
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.
Over stdio, Node 22 or newer.
claude mcp add sentinel \
--env SENTINEL_API_KEY=sk-snt_… \
-- npx -y -p @secondlayer/sentinel sentinel-mcp{
"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.
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 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.
{
"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.
{
"$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"
}
}
}The agent skill (npx skills add https://runsentinel.app) teaches the same rules; see Quickstart.
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.
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.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.
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_….
| 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 |
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 <target> <fn|@admin|@value|@control> [--caller X] [--new-caller] | transfer <target> <stx|token|> --in|--out [--over N|20%] [--within 1h] | mint <token> <name> [--new-minter|--over N] | print <target> <topic|> | state <target> <read> [--drops 20% --within 1h|--crosses N] | deploy <address> | sbtc <stall|payout|mint|rotation|protocol-update|reserve|peg> [--gap N] [--open N] [--after 6h] | pox5 <bond-setup|admin|early-exit>; [--to <ref>] [--note "…"] [--check "<q>"] [--hold]. wait ≤60s. | Monitoring: write |
every | line, confirm?, dry_run? | On a clock: <6h|1d|monday 09:00|cron> [<target>] --digest [--ask "<q>"] | --check "<q>" [--tz Z] [--to <ref>]. No target: all your watches. | Monitoring: write |
at | line, confirm?, dry_run? | Once: <iso> [<target>] --digest | --check "<q>" [--to <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| 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 |
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.