Repository navigation
Push the status dashboard's verdict to a Statuspage component - #151
Conversation
status.internetcomputer.org is a hosted Atlassian Statuspage: its components live in the Statuspage service, not in a repository, so the way to surface MCP health there is to push it. Add an optional pusher to the status dashboard that maps each probe run's overall verdict onto one Statuspage component and PATCHes it through the Statuspage REST API. - monitoring/mcp-status/statuspage.js: the pusher. Mapping: pass -> operational, warn -> degraded_performance, fail -> partial_outage, or major_outage when every MCP endpoint check failed (server down or unreachable). Pushes are change-driven (plus once at startup, since a restarted process cannot know the remote state), the interval is floored at 15 s, overlapping ticks join the in-flight run, push failures are logged (API key redacted) and retried, and the timer is unref'd so the pusher never keeps the process alive. - monitoring/mcp-status/server.js: start the pusher when the three STATUSPAGE_* variables are set; off by default, so existing deployments are unaffected. It reuses the /api/status probe cache, so it adds at most one probe run per interval. - monitoring/mcp-status/statuspage.test.js: unit tests (stubbed fetch, no network) for the mapping, config resolution, the PATCH request shape, change-only pushes, and retry-after-failure. - deploy/native/imcp-status.service: load an optional /etc/imcp-status/statuspage.env (created by the operator, mode 600) so the API key stays out of the repository and the rendered unit. - monitoring/mcp-status/README.md: setup (create the component in the Statuspage admin, mint an API key), variables, and the mapping table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWw9GUtUFEcTi75SnfnPi3
There was a problem hiding this comment.
Pull request overview
Adds optional publishing of MCP dashboard verdicts to Atlassian Statuspage.
Changes:
- Maps dashboard verdicts to Statuspage component states.
- Adds periodic, change-driven Statuspage updates and configuration.
- Documents deployment setup and adds unit tests.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
monitoring/mcp-status/statuspage.js |
Implements Statuspage publishing. |
monitoring/mcp-status/statuspage.test.js |
Tests mapping, configuration, and retries. |
monitoring/mcp-status/server.js |
Starts the optional pusher. |
monitoring/mcp-status/README.md |
Documents configuration and mapping. |
deploy/native/imcp-status.service |
Loads optional Statuspage credentials. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Three review findings, each with a regression test: - Cap the push interval below Node's signed 32-bit timer limit. setInterval coerces larger delays to 1 ms, so an uncapped oversized STATUSPAGE_PUSH_INTERVAL_MS would fire near-continuously - the exact opposite of the minimum-interval floor's intent. - Bound the Statuspage PATCH with AbortSignal.timeout (10 s). A stalled request kept the tick's in-flight promise pending forever, and since later ticks join the in-flight run, the pusher would wedge and the public component would stay stale indefinitely. - Redact the API key from error messages before truncating them. Capping first could cut the message mid-key, leaving a prefix that no longer matched the full-key replacement and so leaked into the log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWw9GUtUFEcTi75SnfnPi3
major_outage was unreachable in a real total outage: the every-check- failed test ran over the whole endpoints section, but that section's auxiliary checks deliberately do not fail when the server is unreachable - version and metadata-consistency degrade to "warn", and mcp-tls can stay "pass" behind a healthy reverse proxy fronting a dead application - so the verdict was always partial_outage. Compute the verdict from an explicit set of service-availability check ids instead (the endpoint probes that report "fail" on a failed request: landing page, discovery documents, the /mcp challenge, and the OAuth endpoints). Add a realistic total-outage report test in which the auxiliary checks are warn/pass and every availability probe fails, and document the distinction in the README's mapping table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWw9GUtUFEcTi75SnfnPi3
Node's fetch (Undici) keeps the underlying connection associated with an unread response body until garbage collection. The pusher never reads the body, and a persistent error response - exactly the case that is retried every interval - would therefore accumulate connections over time. Cancel the body before checking the status, on success and error alike; its content stays out of thrown errors as before, since it is under the remote service's control. Regression test asserts the body is cancelled on both a 200 and an error response. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWw9GUtUFEcTi75SnfnPi3
Review feedback: checking res.status !== 200 was stricter than needed. Use res.ok instead - the API returns 200 today, but a successful push should not depend on that staying exact. Test stubs now carry ok like a real Response, and a new test asserts a 204 is accepted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWw9GUtUFEcTi75SnfnPi3
The pusher ran the full probe suite every interval, and the suite's Dynamic Client Registration check mints and persists a client_id on the monitored server per run - about 1,440 junk registrations a day at the default 60 s interval, cycling the server's 10,000-entry LRU client store in under a week and churning its persistence. - checks.js: checkMcpEndpoints (and runDashboard) accept mutating:false, which skips only the loopback DCR probe - the one check that changes server state. The allow-list probe stays in both modes: its non-allow-listed redirect is rejected before anything is stored. - server.js: the pusher reuses a recent visitor-triggered full report when one exists (already paid for) and otherwise probes with mutating:false. The non-mutating report is not stored in the visitor cache, which promises the full suite. - checks.test.js: non-mutating mode sends no loopback registration, omits the oauth-register check, and still runs the allow-list probe. - README: document that the pusher's own runs are non-mutating and that DCR health is still verified by interactive visits and CLI runs. The verdict mapping is unaffected: it already computes major_outage from the availability checks present in the report. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWw9GUtUFEcTi75SnfnPi3
Two review findings: - Run imcp-status under a dedicated system user (created by deploy.sh) instead of the shared ec2-user. The dashboard process can carry a Statuspage API key in its environment, and same-UID processes can read each other's /proc/<pid>/environ, so sharing a user with the internet-facing imcp2 service would let a compromise of the main service recover the secret despite the root-only env file. The shipped files under /opt/imcp2 are world-readable, so no ownership changes are needed. - Fix an off-by-one in the redaction regression test: with 289 prefix characters the 11-character key occupied indices 289-299 and survived slice(0, 300) whole, so the vulnerable truncate-before-redact ordering passed the test too. With 290 the key straddles the cap and only an unmatchable prefix would survive, which the test now catches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWw9GUtUFEcTi75SnfnPi3
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
monitoring/mcp-status/statuspage.js:145
- This returns silently whenever all three required values are absent, even if an optional pusher variable such as
STATUSPAGE_PUSH_INTERVAL_MSorSTATUSPAGE_API_BASEis set. That contradicts the documented contract that only a completely unset configuration is silent and makes this partial/mistyped deployment impossible to diagnose. Include the optional variables when deciding whether any Statuspage configuration was supplied.
if (missing.length === 3) return {};
Three review findings: - Factor sanitizeForLog out of server.js into log.js and reuse it from statuspage.js's describeError, which carried a near-identical copy. describeError still redacts the API key on the full message BEFORE the shared sanitiser truncates it. Small log.test.js covers the sanitiser directly. - Bound the pusher's report reuse by the dashboard cache TTL as well as the push interval. Reusing anything /api/status already considers expired could let a tick publish stale state, delaying an outage by up to two intervals. - Join a visitor-triggered full probe run that is already in flight instead of starting a second concurrent suite; the full report satisfies the pusher, and probe load stays coalesced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWw9GUtUFEcTi75SnfnPi3
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
monitoring/mcp-status/README.md:148
- This sentence is grammatically incorrect: “make … as merely partial” should be “make … appear merely partial.”
outage report as merely partial.
The dedicated imcp-status UID alone did not isolate the Statuspage API key: the dashboard source was staged under /opt/imcp2 owned by the deploy/SSH user, the same UID the internet-facing imcp2.service runs as, so a compromised main service could edit server.js and have the next dashboard restart execute its code in the secret-bearing service. deploy.sh now installs a root-owned, non-group/other-writable copy of monitoring/mcp-status at /opt/imcp-status on every deploy, and the unit executes that copy. The staged tree under /opt/imcp2/monitoring remains the shipping vehicle only. Deploy README's layout diagram updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWw9GUtUFEcTi75SnfnPi3
resolveStatuspageConfig stayed silent whenever the three required variables were absent, even when an optional one ( STATUSPAGE_PUSH_INTERVAL_MS or STATUSPAGE_API_BASE) was set - so a mistyped deployment intending to run the pusher got a silent no-op instead of a diagnosis, contradicting the documented contract that only a completely unset configuration is silent. Warn in that case, naming both the optional variables that were set and the required ones that are missing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWw9GUtUFEcTi75SnfnPi3
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
monitoring/mcp-status/statuspage.js:146
- When only an optional override such as
STATUSPAGE_PUSH_INTERVAL_MSorSTATUSPAGE_API_BASEis set,missing.lengthis still 3 and this returns silently. That hides a partial configuration despite this function's documented warning behavior. Treat the pusher as entirely unset only when none of the required or optional Statuspage variables has a value.
if (missing.length === 3) {
The documented setup step - placing statuspage.env in /etc/imcp-status (mode 600) - could not be completed on a fresh host without an undocumented mkdir, and since the EnvironmentFile is optional, a restart after the attempt would silently leave the pusher disabled. Create the directory root-only (mode 700) in deploy.sh; systemd reads the EnvironmentFile as root, so no other user needs access. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWw9GUtUFEcTi75SnfnPi3
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (1)
deploy/native/deploy.sh:123
- The secret-bearing copy is still sourced from
$REMOTE_DIR, which is writable by the sameec2-useras the compromised main service this isolation is meant to contain. That process can replaceserver.js(or race the copy after the tar upload), andcp -awill copy the malicious file—preserving symlinks as well—before root ownership is applied; the subsequent restart then runs it with access to the Statuspage key. Stream/extract the dashboard directly from the trusted deployment input into a root-owned temporary directory and atomically install it, rather than promoting files from the untrusted staging tree.
# which is also the UID the internet-facing imcp2.service runs as — a
# compromised main service could edit server.js there and have the next
# dashboard restart run its code in the secret-bearing service. Refresh the
# copy on every deploy; strip group/other write (files stay world-readable, so
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
monitoring/mcp-status/checks.js:186
mutating: falsestill sends the allow-list registration request. If the guard regresses—the exact failure this probe is meant to detect—the server reaches the normal registration path and persists a new client (src/auth.rs:1913-1918). The unattended 60-second pusher would then create about 1,440 clients/day and churn the 10,000-entry LRU. Skip this probe in non-mutating runs as well, or provide a genuinely dry-run validation path; keep it in interactive/full runs.
* @param {{ mutating?: boolean }} [opts] With `mutating: false`, skip the one
* check that changes server state — the loopback Dynamic Client Registration
* probe, which mints and persists a fresh client_id in the server's
* LRU-bounded registration store on every run. Interactive uses (a person
* loading the dashboard, a one-off CLI run) keep the default; unattended
* periodic callers (the Statuspage pusher) must not grind through that store.
* The allow-list check stays in both modes: its non-allow-listed redirect is
* rejected before anything is stored, so it never mutates.
Non-mutating mode still sent the allow-list registration request. While the server's hosted-redirect guard holds that stores nothing, but if the guard regressed - the exact failure the probe detects - every run would reach the normal registration path and mint a client, turning the unattended pusher into a store-grinding loop precisely when the server is broken. Non-mutating runs now skip both probes that POST to /oauth/register; interactive dashboard visits and CLI runs still exercise both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWw9GUtUFEcTi75SnfnPi3
runDashboard's JSDoc and the getPusherReport comment still said non-mutating mode skips only the DCR probe; it skips both registration probes, and the resulting report is missing two checks - which is also why it must not enter the visitor cache. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWw9GUtUFEcTi75SnfnPi3
Summary
Puts MCP health on status.internetcomputer.org. That page is a hosted Atlassian Statuspage — its components live in the Statuspage service, not in any repository — so the way to surface MCP status there is to push it. This PR adds an optional, off-by-default pusher to the status dashboard (
monitoring/mcp-status) that maps each probe run's overall verdict onto one Statuspage component and PATCHes it through the Statuspage REST API.Context: the human-facing MCP status dashboard is mirrored at
internetcomputer.org/icp-mcp/status/, but its live-data API is not reachable from that origin yet, andmcp.internetcomputer.orgis being handed over to the IC gateway. Publishing the verdict to the official status page gives MCP a monitoring surface that is independent of both migrations (per review discussion on dfinity/internetcomputer-org#71).Mapping (deliberately coarse — the public status page is not the detailed dashboard):
passoperationalwarndegraded_performancefail, some MCP endpoint checks still passingpartial_outagefailwith every MCP endpoint check failingmajor_outageRelated issues
Follow-up to the
/mcp→/icp-mcplanding-page work in dfinity/internetcomputer-org#71 and #72 (status dashboard's data API not reachable from the mirror's origin).Changes
monitoring/mcp-status/statuspage.js— the pusher: verdict mapping, env-based config (STATUSPAGE_PAGE_ID,STATUSPAGE_API_KEY,STATUSPAGE_COMPONENT_ID, optionalSTATUSPAGE_PUSH_INTERVAL_MSfloored at 15 s), change-driven PATches (plus one at startup, since a restarted process cannot know the remote state), overlapping ticks join the in-flight run, failures logged (API key redacted, no response body) and retried next interval, timerunref'd.monitoring/mcp-status/server.js— starts the pusher only when all threeSTATUSPAGE_*variables are set; a partial configuration logs a warning naming the missing variables instead of silently not pushing. Reuses the/api/statusprobe cache, so it adds at most one probe run per interval.monitoring/mcp-status/statuspage.test.js— unit tests (stubbedfetch, no network): mapping, config resolution/flooring, PATCH request shape (Authorization: OAuth …), change-only pushes, retry-after-failed-push, probe-failure survival, no key leakage in errors.deploy/native/imcp-status.service— loads an optional/etc/imcp-status/statuspage.env(EnvironmentFile=-…, created by the operator, mode 600), keeping the API key out of the repo and the rendered unit.monitoring/mcp-status/README.md— setup and mapping documentation.One-time ops step (not in this PR): in the Statuspage admin, create the component (e.g. "ICP MCP server"), mint an API key, and place the three variables in
/etc/imcp-status/statuspage.envon the deployed host, thensystemctl restart imcp-status.Testing
cargo build --locked --all-targetscargo test --locked --all-targetscargo fmt --all/cargo clippy --all-targets(tree is warning-free)npm test --prefix monitoring/mcp-status(if the dashboard changed) — 40/40 pass (26 existing + 14 new)No Rust code is touched, so the cargo items are unaffected (CI runs them regardless).
Checklist
Generated by Claude Code