diff --git a/.github/workflows/deploy-native.yml b/.github/workflows/deploy-native.yml index 752e546..1e5b5c6 100644 --- a/.github/workflows/deploy-native.yml +++ b/.github/workflows/deploy-native.yml @@ -241,6 +241,10 @@ jobs: # Expose the beta II instance at /mcp-beta on staging only; production # serves /mcp (production II) alone. Empty string reads as off. MCP_SERVE_BETA: ${{ inputs.environment == 'staging' && '1' || '' }} + # Publish the status dashboard at /status/ on staging only. Production + # keeps running the imcp-status service (its Statuspage pusher feeds + # status.internetcomputer.org) but does not expose it at its origin. + SERVE_STATUS: ${{ inputs.environment == 'staging' && '1' || '' }} # OpenAI Apps domain verification (ChatGPT-directory submission). # Unset -> endpoint 404s. An optional secret that neither caller # passes arrives here as the empty string. diff --git a/.github/workflows/health.yml b/.github/workflows/health.yml index 59d0efd..8a3c4a5 100644 --- a/.github/workflows/health.yml +++ b/.github/workflows/health.yml @@ -174,7 +174,7 @@ jobs: ":rotating_light: *ICP MCP production health check failed*\n" f"Failing: {os.environ.get('FAILED', 'unknown')}\n" f"<{sys.argv[1]}|Run log> · " - "" + "" ) json.dump({"text": text}, sys.stdout) PY diff --git a/deploy/native/Caddyfile b/deploy/native/Caddyfile index c26c7fe..c6dcb20 100644 --- a/deploy/native/Caddyfile +++ b/deploy/native/Caddyfile @@ -21,13 +21,22 @@ } } __DOMAIN__ { - # Status dashboard (internal Node service on :8137), published under /status/. + # Status dashboard (internal Node service on :8137), published under /status/ + # on STAGING ONLY. deploy.sh keeps the block between the two STATUS marker + # comment lines below when SERVE_STATUS is set (deploy-native.yml sets it + # for the staging environment) and deletes it otherwise, so production does + # not publish the dashboard at its origin: the public status surface is + # status.internetcomputer.org, which the dashboard's Statuspage pusher feeds. + # The imcp-status service itself still runs on every host (see deploy.sh); + # only its exposure at /status/ is staging-only. # handle_path strips the /status prefix, so the dashboard sees /, /api/status, # etc. as if mounted at the root; its assets are loaded path-relative. + # __STATUS_BEGIN__ redir /status /status/ 308 handle_path /status/* { reverse_proxy 127.0.0.1:8137 } + # __STATUS_END__ # The app serves /metrics on its own port (imcp2.service sets # MCP_SERVE_METRICS=1); a scraper reaches it on the host's private address. diff --git a/deploy/native/README.md b/deploy/native/README.md index 4c0d592..b1a376f 100644 --- a/deploy/native/README.md +++ b/deploy/native/README.md @@ -27,9 +27,12 @@ mismatch — otherwise the unit installs fine and then crash-loops on your domain and reverse-proxies to it, obtaining a Let's Encrypt cert automatically. The **status dashboard** (`monitoring/mcp-status`) is also shipped and run as a -Node systemd service (`imcp-status.service`) bound to `127.0.0.1:8137`. Caddy -publishes it at **`https://$DOMAIN/status/`**, where it probes the deployment's -own public endpoints and the linked Internet Identity instance. Node ≥ 20 is +Node systemd service (`imcp-status.service`) bound to `127.0.0.1:8137`. On +**staging only**, Caddy publishes it at **`https://$DOMAIN/status/`**, where it +probes the deployment's own public endpoints and the linked Internet Identity +instance; production runs the same service (its Statuspage pusher feeds the +public [status.internetcomputer.org](https://status.internetcomputer.org/)) but +does not expose it at its origin (see `SERVE_STATUS` below). Node ≥ 20 is installed automatically on first deploy if absent; the dashboard has no build step and no third-party dependencies. @@ -180,6 +183,13 @@ substituted into the unit by `deploy.sh`), so staging serves the beta Internet Identity instance at `/mcp-beta` alongside the production `/mcp`. Production leaves it unset, so it serves `/mcp` (production II) alone. +Staging alone also sets `SERVE_STATUS=1`. `deploy.sh` then keeps the Caddyfile's +`/status/` block (between its `__STATUS_BEGIN__`/`__STATUS_END__` markers), so the +status dashboard is published at `https:///status/`; for production +the block is deleted and the dashboard is not published at the origin. The public +status surface for ICP MCP is [status.internetcomputer.org](https://status.internetcomputer.org/), +driven by the dashboard's Statuspage pusher, which keeps running on every host. + Both hosts set `MCP_SERVE_METRICS=1` (hardcoded in the unit), so the app serves the Prometheus exposition at `/metrics` on its own port for a scraper to reach on the host's private address. Caddy answers `/metrics` on the public origin with a @@ -337,8 +347,9 @@ sudo journalctl -u caddy -f # TLS / cert logs sudo journalctl -u imcp-status -f # status dashboard logs ``` -The dashboard is at `https:///status/` and shows staging and production -side by side (the same two columns on every host). To change the monitored set, +The dashboard is published at `https:///status/` only (see +`SERVE_STATUS` above; the public status page is status.internetcomputer.org) and +shows staging and production side by side. To change the monitored set, its II pins or the SSRF allowlist, set `STATUS_TARGETS` / `STATUS_TARGET_II` for `deploy.sh` or edit the `Environment=` lines in `/etc/systemd/system/imcp-status.service` and `systemctl restart imcp-status`. diff --git a/deploy/native/deploy.sh b/deploy/native/deploy.sh index 47daf3d..5e012af 100755 --- a/deploy/native/deploy.sh +++ b/deploy/native/deploy.sh @@ -81,7 +81,19 @@ echo ">> rendering + installing units and Caddyfile, then (re)starting services" # MCP_SERVE_BETA is set (to "1") only for the staging deployment, so /mcp-beta # is exposed there and not in production; it defaults to empty (off) otherwise. unit_mcp="$(sed -e "s#__PUBLIC_URL__#https://$DOMAIN#g" -e "s#__MCP_SERVE_BETA__#${MCP_SERVE_BETA:-}#g" -e "s#__OPENAI_APPS_CHALLENGE_TOKEN__#${OPENAI_APPS_CHALLENGE_TOKEN:-}#g" "$here/imcp2.service")" -caddyfile="$(sed -e "s#__DOMAIN__#$DOMAIN#g" -e "s#__ACME_EMAIL__#$ACME_EMAIL#g" "$here/Caddyfile")" +# SERVE_STATUS likewise is set (to "1") only for the staging deployment. Staging +# keeps the Caddyfile's marked /status/ block (minus the marker lines) so the +# dashboard is published at https://$DOMAIN/status/; production deletes the whole +# block, so its origin does not publish the dashboard -- the public status surface +# is status.internetcomputer.org. The imcp-status service is still installed and +# started on every host below: the Statuspage pusher that feeds that page runs +# inside it, and only the dashboard's exposure at /status/ is staging-only. +if [ -n "${SERVE_STATUS:-}" ]; then + status_block=(-e '/__STATUS_BEGIN__/d' -e '/__STATUS_END__/d') +else + status_block=(-e '/__STATUS_BEGIN__/,/__STATUS_END__/d') +fi +caddyfile="$(sed -e "s#__DOMAIN__#$DOMAIN#g" -e "s#__ACME_EMAIL__#$ACME_EMAIL#g" "${status_block[@]}" "$here/Caddyfile")" caddy_unit="$(cat "$here/caddy.service")" # The dashboard shows the monitored instances side by side. Every host renders # the same set by default -- staging and production -- so the page reads the same diff --git a/deploy/native/imcp-status.service b/deploy/native/imcp-status.service index 4226c2e..e1c886d 100644 --- a/deploy/native/imcp-status.service +++ b/deploy/native/imcp-status.service @@ -23,8 +23,10 @@ WorkingDirectory=/opt/imcp-status # pinned via MCP_STATUS_TARGET_II. The allowlist is pinned to this host plus each # target's host (the dashboard always permits the built-in id.ai suffixes on # top), so the probes can never be steered elsewhere -- including by a server's -# own /version. Binds to 127.0.0.1; Caddy publishes it under -# https:///status/. +# own /version. Binds to 127.0.0.1. Caddy publishes it under +# https:///status/ on staging only (SERVE_STATUS in deploy.sh); the +# production host runs this unit too -- its Statuspage pusher feeds the public +# status.internetcomputer.org -- but does not publish the dashboard at its origin. Environment="MCP_STATUS_TARGETS=__TARGETS__" Environment="MCP_STATUS_TARGET_II=__TARGET_II__" Environment=MCP_STATUS_ALLOWED_HOSTS=__ALLOWED_HOSTS__ diff --git a/docs/anthropic-directory-submission.md b/docs/anthropic-directory-submission.md index 8324ced..403bee6 100644 --- a/docs/anthropic-directory-submission.md +++ b/docs/anthropic-directory-submission.md @@ -71,7 +71,7 @@ submission — and match a live scan of a deployed instance of that build | Support channel | ✅ (shown on every error screen) | | Security-vulnerability reporting mechanism (a Software Directory Terms obligation) | ✅ [`SECURITY.md`](../SECURITY.md) → Hackenproof bug bounty | | Public documentation by publish date | ✅ this repo's README + the landing page at (its one home, maintained in dfinity/internetcomputer-org; `https://mcp.internetcomputer.org` permanently redirects there from the release that ships #165) | -| Status/health visibility | ⚠️ is currently cut off: since the origin moved behind the gateway front it answers with a redirect to the landing page instead of the dashboard (observed 2026-09-01). Have the fronting layer forward `/status/` (and `/version`, which the dashboard and the health workflow read), or point the listing at a reachable status page, before submitting | +| Status/health visibility | ✅ — the **ICP MCP** component on the ICP Statuspage, driven by the deployment's status dashboard (`monitoring/mcp-status`) through its Statuspage pusher. This is the only public status surface: the dashboard itself is published on staging alone (`SERVE_STATUS`, see `deploy/native/README.md`), not on `mcp.internetcomputer.org` (whose origin answers `/status/` with a redirect at the gateway) nor under `internetcomputer.org/icp-mcp/` | Notes on auth mode: pure M2M `client_credentials` is unsupported by Claude (every connection needs a user in the loop) — IMCP2's user-consent flow via diff --git a/monitoring/mcp-status/README.md b/monitoring/mcp-status/README.md index d3b4839..3064ab6 100644 --- a/monitoring/mcp-status/README.md +++ b/monitoring/mcp-status/README.md @@ -188,8 +188,12 @@ the host and runs it as the `imcp-status.service` systemd unit (bound to production's II pinned to `https://id.ai` — so every host's dashboard shows the same two columns; `STATUS_TARGETS` / `STATUS_TARGET_II` in `deploy.sh`'s environment override the set, and the allowlist is widened to each target's -host automatically. Caddy publishes it at `https:///status/`, and the CI -workflow runs the unit tests below before rolling out. See the deploy README. +host automatically. Caddy publishes it at `https:///status/` on **staging +only** (`SERVE_STATUS`, set by `deploy-native.yml` for that environment); the +production host runs the unit as well — its Statuspage pusher below feeds the public +[status.internetcomputer.org](https://status.internetcomputer.org/) — but does not +publish the dashboard at its origin. The CI workflow runs the unit tests below before +rolling out. See the deploy README. ### Publishing to an Atlassian Statuspage (status.internetcomputer.org) diff --git a/src/main.rs b/src/main.rs index ab925a3..916fc2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -101,8 +101,10 @@ fn serve_metrics() -> bool { /// This origin answers their old paths with permanent redirects instead of /// copies, so every published link keeps working — the directory listings' /// policy URLs, old bookmarks, search results — while the content exists -/// exactly once. `/status/` is unaffected: the live dashboard is this -/// deployment's own monitoring surface, published by the fronting proxy. +/// exactly once. `/status/` is not among them and is not redirected here: the +/// status dashboard is published by the fronting proxy on staging only (see +/// `SERVE_STATUS` in deploy/native); production's origin does not publish it, +/// and the public status surface is status.internetcomputer.org. const LANDING_SITE: &str = "https://internetcomputer.org/icp-mcp"; /// The page paths this origin used to serve, each answered with a permanent