npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vitalcheck-mcp

v0.12.0

Published

VitalCheck MCP server — lets AI agents drive the platform (status, monitors, host agents, alerts) over a scoped API key.

Readme

vitalcheck-mcp

MCP server for VitalCheck — lets AI agents (Claude Code, Claude Desktop, any MCP client) drive your monitoring: read org health, manage monitors, configure host agents over the air, and manage alerting. Authorization is enforced server-side by your API key's scopes, so the AI can only do what the key allows.

Setup — one command

Create an API key in VitalCheck → Security → API keys and tick the scopes the AI may use — the key-created screen shows this command ready to copy, key baked in:

claude mcp add vitalcheck \
  -e VITALCHECK_URL=https://app.vitalcheck.io \
  -e VITALCHECK_API_KEY=vck_… \
  -- npx -y vitalcheck-mcp

Works on every OS. The key lives only in your MCP config after this — revoke it in VitalCheck anytime to cut the AI off.

Any other MCP client works with the equivalent config:

{
  "mcpServers": {
    "vitalcheck": {
      "command": "npx",
      "args": ["-y", "vitalcheck-mcp"],
      "env": {
        "VITALCHECK_URL": "https://app.vitalcheck.io",
        "VITALCHECK_API_KEY": "vck_…"
      }
    }
  }
}

Scopes

Pick per key, least-privilege:

| Scope | Grants | | --- | --- | | read:status | read monitors, agents, events, org summary (most tools need this) | | manage:monitors | create / edit / delete monitors, trigger checks | | manage:agents | enroll hosts, edit OTA metric configs, rename | | manage:alerts | notification channels + alert rules | | manage:secrets | list / create / delete probe secrets (never read a value) | | ingest:events | record deploy / custom events with record_event |

A tool the key can't use returns a clear 403 … lacks scope … the AI can relay.

Tools

Read: org_status, list_monitors, monitor_history, monitor_forecast, list_dynamic_alerts, list_agents, list_events, list_captures, explore_factors, explore_pivot, list_alert_rules, list_channels, org_settings, list_secrets, preview_expression, summary_panels, list_summary_dashboards

Managecreate_monitor, create_derived_monitor, update_monitor, delete_monitor, bulk_tag_monitors, run_check, enroll_agent, set_agent_metrics, rename_agent, test_agent_metric, capture_host_evidence, create_alert_rule, set_dynamic_alert, update_org_settings, create_secret, delete_secret, create_summary_dashboard, update_summary_dashboard, delete_summary_dashboard

Record: record_event

Host evidence

list_captures returns what the agent recorded on the host when a cpu, memory, disk or load metric breached: top processes, memory, disk, and a diff against the last healthy snapshot. Ask by monitorId when that metric is the one that breached; ask by agentId with a from/to bracket for anything else, since an availability or custom monitor never captures and only the host timeline can say what was running. top holds executable names only, never arguments.

capture_host_evidence asks an agent monitor's host for one now: it collects on its next check-in and is then readable with list_captures.

Deploy markers

record_event puts a release on the timeline, which is what lets you read a spike against the deploy that caused it. The event becomes a row in Activity and a marker tick on every chart of any investigation covering that moment.

record_event
  type: "deploy"      title: "checkout: switch to the pooled client"
  sha: "b861e7d"      version: "v1.4.0"    ref: "main"
  env: "production"   actor: "ci"          outcome: "success"
  ts:  "2026-09-02T10:11:12Z"
  url: "https://github.com/acme/app/actions/runs/1234"

Call it at the end of your own deploy pipeline, or right after the AI ships something. Events are append-only, so a wrong title cannot be corrected later.

Explore

explore_factors answers what the things that break have in common. For every tag value it compares that value's share of the incidents against its share of the fleet, and lift is the ratio: a lift of 4 means four times more incidents than the group's size predicts. Rank by lift, never by raw count.

explore_factors
  window: "7d"   count: "breach"
→ { "incidents": 61, "subjects": 44,
    "factors": [{ "key":"team", "value":"payments", "incidents":22,
                  "incidentShare":0.36, "fleetShare":0.09, "lift":4 }],
    "anchors": [{ "id":"deploy", "label":"Deploys", "count":9 }] }

anchors is what the window actually has to anchor against, so call again with one of those ids to test whether the incidents cluster in the lag minutes after the event: the anchor block then carries after, afterShare, timeShare and its own lift. Ids are none (the default), deploy, agent (agent restarts), custom:<title> and monitor:<uuid>. The deploys record_event puts on the timeline are what make the first of those useful.

explore_pivot crosses any two dimensions against a measure. A dimension is a tag key or hour (hour of day in tz), and the measure is incidents, minutes, monitors or after:

explore_pivot
  window: "30d"   rows: "team"   cols: "hour"
  measure: "incidents"   tz: "Europe/Athens"
→ { "cells": [{ "r":"payments", "c":"03", "value":7, "subjects":0 }],
    "rowTotals": {…}, "colTotals": {…}, "total": 61 }

Hours are zero-padded strings ("03"), so they sort like every other axis value. subjects is the fleet population behind a cell, and it is 0 on an hour axis: a monitor does not live in an hour.

The empty-string value means untagged. Margins come back computed in rowTotals and colTotals, so don't add the cells up yourself. Each axis is capped at its first 40 values, so on a wider tag key total counts only the cells on the grid and can be lower than the incidents in the window. rows and cols must differ (refused before the request), and an unknown tag key is a 400.

Alert rules

create_alert_rule matches events on the stream and delivers to channels. Scope by tagFilters rather than monitorId wherever you can, so monitors tagged later are covered without touching the rule.

create_alert_rule
  name: "prod breaches → on-call"
  eventTypes: ["state_transition"]   minSeverity: "error"
  tagFilters: { "env": ["prod"] }    channelIds: [...]
  throttleSec: 600                   notifyOnRecovery: true

notifyOnRecovery (default true) sends a resolved notice when a monitor this rule fired for returns to ok, so an incident closes itself instead of leaving someone watching a screen for the all-clear. Set it false for a rule whose firing is not an incident anyone waits out.

Dynamic alerts

A dynamic alert fires on where a metric is heading, not on where it is. After three days of history every numeric monitor gets a profile:

  • fill moves in a straight line toward its bad threshold, the way a disk fills or a queue grows. The question is when it arrives.
  • cycle repeats a daily shape, the way traffic or CPU does. The question is whether it is outside the range that shape predicts.
  • none fits neither, and pending has not been profiled yet.

Five tools:

| Tool | Does | Scope | | --- | --- | --- | | list_dynamic_alerts | the whole org at once: watched, suggested, openEpisodes | read:status | | monitor_forecast | profile, reason, ETA, shadow count, and the {ts,mid,lo,hi} curve | read:status | | set_dynamic_alert | enabled, mode: "live", horizonHours (1 to 720) for one monitor | manage:alerts | | org_settings | reads dynamicAlerts, the workspace switch | read:status | | update_org_settings | flips dynamicAlerts for the whole org (admin) | manage:alerts |

monitor_forecast   monitorId: "…"   window: "24h"
→ { "profile":"fill", "reason":"climbs 2.6 %/day toward 90 %",
    "enabled":false, "mode":"shadow", "expectedBreachAt":"2026-09-11T04:00:00Z",
    "horizonHours":168, "shadowFired":0, "curve":[…] }

set_dynamic_alert  monitorId: "…"   enabled: true

Start from the fleet, not a monitor id, when the question is "what are we watching" or "what should we watch":

list_dynamic_alerts
→ { "watched":[{ "name":"disk / on web-1", "unit":"%", "profile":"fill", "mode":"shadow",
                 "expectedBreachAt":"2026-09-11T04:00:00Z", "shadowFired":2 }],
    "suggested":[…], "openEpisodes":1 }

One request for the whole org. suggested is what set_dynamic_alert is for, and openEpisodes counts the watched monitors alerting right now: an anomaly that has not cleared, or an ETA inside the horizon. These are the three lists the Forecast tab shows in the app.

Shadow first. Enabling starts a 7 day shadow period: events are still written, but with severity info, a shadow flag and the title prefix Would alert: , and nothing is delivered to a channel. Read what the alert would have said, then pass mode: "live" to end the shadow early, or leave it and the worker goes live by itself after 7 days.

The two event types are forecast_breach (on trend to reach the bad threshold inside the horizon) and forecast_anomaly (outside its usual daily range for two readings in a row). Both are matchable by create_alert_rule, and both close themselves with a recovery event. Shadow events are info, so a rule at the default warn floor ignores a monitor until its alerts go live.

A 409 from set_dynamic_alert is the API refusing on purpose: Not enough data to forecast this monitor yet means the profile is pending or none, so wait rather than retry, and Dynamic alerts are off for this workspace means the org switch is off.

Bulk tagging

Tags are what make a fleet filterable, alertable (create_alert_rule scopes by tag) and groupable, so retagging in bulk is usually step one:

list_monitors  search: "checkout"
bulk_tag_monitors
  monitorIds: [...]        set: { "team": "payments", "env": "prod" }

merge (the default) upserts those keys and leaves the rest alone; remove strips keys; mode: "replace" makes set the whole tag map and drops everything else. Agent-shipped monitors are retagged through their metric def, so the tag survives the host's next check-in instead of reverting.

The call returns { updated, unchanged, skipped, agentDefsUpdated, ... } and a 200 does not mean everything changed — anything unwritable lands in skipped with a reason (not_found, agent_unresolved, agent_inherited). Read it.

Summary boards

summary_panels answers fleet questions without saving anything: describe the panels, get the numbers back. This is how you get a rollup sliced by tag — one vitality per env, per team, per region — which org_status cannot do.

summary_panels
  window: "30d"   tags: { "env": "prod" }
  panels: [{ "id":"by-team", "type":"tag_breakdown", "w":8, "h":2,
             "scope":{}, "groupBy":"team", "metric":"all", "sort":"vitality" }]

When a layout is worth keeping, save it with create_summary_dashboard — a board is org-wide, and isDefault picks the one the Summary screen opens on.

| Widget type | Its own fields | | --- | --- | | vitality | compare, display number|dial | | status_counts | show | | tag_breakdown | groupBy (required), metric, sort, limit, includeUntagged | | incidents_chart | minSeverity, types, showTotal | | top_offenders | minSeverity, limit | | monitor_chart | monitorIds (1–6, required), historyWindow, mode, showThresholds | | monitor_stat | monitorId (required), stat, historyWindow, sparkline | | monitor_list | sort, limit, columns | | event_feed | minSeverity, types, limit | | matrix | rowKey + colKey (must differ), cellMetric | | text | body, emphasis |

Every widget also takes id (your slug, unique on the board), title, w (1–12), h (1–6), window, and a scope of { tags, source, agentId, search, status } that is ANDed with the request-level tags. Panels that cannot be answered come back as { id, error } inside a 200; the rest still resolve.

update_summary_dashboard's widgets replaces the whole array — read the board with list_summary_dashboards first, edit that array, send it back.

Secrets

Secrets let a probe authenticate, so you can monitor an internal API instead of only public endpoints. Store one, then reference it from an HTTP probe header:

create_secret
  name: "OPS_TOKEN"   value: "<the token>"   description: "read-only ops API"

create_monitor
  name: "internal api health"
  probeConfig: { "type":"http", "url":"https://internal.example.com/health",
                 "headers": { "authorization": "Bearer ${secret.OPS_TOKEN}" } }

The monitor stores the reference; the worker substitutes the real value only while sending the request.

There is no tool that reads a secret's value, and there never will be. The API has no endpoint that returns one — a leaked key still cannot exfiltrate a credential. list_secrets gives you names, descriptions and usage counts, which is everything you need to write a reference. delete_secret is refused (with the monitors named) while something still references it, unless you pass force.

Derived monitors

A derived monitor computes its value from other monitors on an interval — e.g. an error-rate % from an error counter and a request counter. Preview the expression against live values first, then create:

preview_expression
  inputs:     [{var:"a", monitorId:"<errors-id>"}, {var:"b", monitorId:"<requests-id>"}]
  expression: "(a / max(b, 1)) * 100"
→ { "value": 0.42, "kind": "number", "env": { "a": 3, "b": 714 } }

create_derived_monitor
  name: "checkout error rate"   unit: "%"
  inputs / expression as above  intervalSec: 60
  valueType: "numeric"          warn: 1   bad: 5   dir: "high"

Inputs are existing non-derived monitors (up to 8, vars ah; find ids with list_monitors). Expressions support arithmetic, comparisons, && || !, if(cond,a,b), min max avg abs round floor ceil clamp, and string helpers concat str num — categorical/boolean results pick their threshold shape via valueType.