Serve production II at /mcp; drop /mcp-prod; add opt-in /mcp-beta for staging - #92
Conversation
… staging The deployment now has separate production and staging environments, each serving a single /mcp endpoint. Previously the binary hardcoded two mounts on every deployment: /mcp (beta II) and /mcp-prod (production II). - /mcp now serves PRODUCTION Internet Identity (id.ai) on every deployment (IiInstance::prod, II_URL_PROD/II_CANISTER_ID_PROD, default id.ai). It is the origin's default instance and owns the plain-root discovery documents. - /mcp-prod is removed entirely. - /mcp-beta serves beta II, opt-in via MCP_SERVE_BETA (off by default), so the staging deployment can offer beta while production serves /mcp alone. /version reports the production gauge always and the beta gauge only when the staging instance is served (zero otherwise). The landing page and design doc now describe /mcp as production; the staging beta endpoint is documented as opt-in. Router-level tests updated to the new shape (prod default at /mcp, beta at /mcp-beta). cargo test: 136 lib + 7 router + 1 doctest passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
There was a problem hiding this comment.
Pull request overview
This PR updates the deployment routing model so /mcp is always the production Internet Identity-backed MCP endpoint, removes the legacy /mcp-prod mount, and introduces an opt-in staging-only /mcp-beta mount controlled by MCP_SERVE_BETA.
Changes:
- Switch
/mcptoIiInstance::prod()(production II) and make it the default instance that owns the plain-root discovery documents. - Remove
/mcp-prodand add optional/mcp-beta(beta II) behindMCP_SERVE_BETA, including correct router composition, logging, shutdown, and/versiongauges. - Update docs, landing page, and tests to reflect the new mount structure.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/main.rs |
Makes /mcp serve production II by default; conditionally adds /mcp-beta; updates /version, logging, and shutdown for optional beta. |
tests/routers.rs |
Reshapes the composed test app to mirror staging (prod at /mcp, beta at /mcp-beta) and updates route assertions accordingly. |
src/lib.rs |
Updates library docs describing multi-instance composition with /mcp + /mcp-beta. |
src/identities.rs |
Updates comments to reflect beta being served at /mcp-beta (staging) and prod at /mcp. |
src/auth.rs |
Updates test fixtures/strings referencing the removed /mcp-prod mount. |
src/assets/index.html |
Updates the landing page endpoint list and connector URL to point to /mcp as production. |
README.md |
Updates run/auth documentation to describe /mcp as production and /mcp-beta as staging-only via MCP_SERVE_BETA. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Resolve two conflicts from main's landing-page redesign (#78/#79) and the INDEX_HTML const change: - src/main.rs: keep `let mut app` (needed for the conditional /mcp-beta mount) and take main's `Html(INDEX_HTML)` (INDEX_HTML is now a &str const, not a LazyLock<String>). - src/assets/index.html: take main's exported design bundle, then re-apply this branch's endpoint change within it — the production connector URL becomes https://mcp.internetcomputer.org/mcp (5 occurrences; /mcp-prod is removed by this branch and must not be advertised), and the beta-testing hint becomes https://mcp.beta.id.ai/mcp-beta. cargo test: 139 lib + 7 router + 1 doctest passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
The auth-challenge block now probes the `/mcp-beta` mount, but its assertion failure message still read "prod challenge", which would misdirect diagnosis of a routing failure. Changed it to "beta challenge". Test-only, no behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
tests/routers.rs:198
- The assertion failure message still says "prod challenge" even though this block is testing the
/mcp-beta(beta) mount. This will be misleading when the test fails.
"beta challenge: {challenge}"
src/identities.rs:64
- This comment ties the beta Internet Identity defaults to a specific mount path (
/mcp-beta).IiInstance::beta()is used by the library regardless of which path the caller mounts it at, so the path-specific wording is misleading.
/// connect-time `/mcp-beta` (staging) handshake (browser). Override with `II_URL`.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
README.md:707
- This section explains that
/mcpuses production Internet Identity (id.ai), but it doesn’t mention that users must enable the deployment origin as a trusted MCP server in their id.ai settings (analogous to the beta.id.ai note below for staging). Without that, first-time users may fail to connect despite correctII_URL_PROD/II_CANISTER_ID_PRODconfig.
Set the public base URL (used in the discovery docs, as the MCP origin, and as the
management identity's derivation origin) with `PUBLIC_URL`. The `/mcp` endpoint is
**production** Internet Identity: `II_URL_PROD` (browser login, default
`https://id.ai`) plus `II_CANISTER_ID_PROD` (the canister the `mcp_*` calls target,
default `rdmx6-jaaaa-aaaaa-aaadq-cai`); both point at the same II.
src/identities.rs:65
- This comment ties
II_URL_DEFAULTto a specific mount path (/mcp-beta), but this is a library-level constant used for the beta Internet Identity instance regardless of where a consumer mounts it. Making it path-agnostic avoids misleading downstream users.
/// Internet Identity instance, single source of truth. Default: **`beta.id.ai`**.
/// A real domain is required: the raw `<canister>.icp0.io` origin is rate-limited
/// (HTTP 429) for the browser login SPA, leaving the II popup blank. Used for the
/// connect-time `/mcp-beta` (staging) handshake (browser). Override with `II_URL`.
const II_URL_DEFAULT: &str = "https://beta.id.ai";
…aging beta #92 merged into main after this branch was last rebased: it made /mcp the production Internet Identity endpoint, removed /mcp-prod, and added an opt-in /mcp-beta (beta II) for staging. The rebase brought main's new wording in cleanly, but this branch's own landing-page sentence still described the old model ("/mcp on beta II, /mcp-prod on production II"). Updated it to name the production /mcp endpoint and note staging's /mcp-beta. No /mcp-prod references remain in the README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
Copilot caught a real break I missed: I updated the terminal renderer but not the hosted web UI. public/index.html read `targets.iiOrigin` and `facts.ii.canisterId`, so on the page Caddy actually serves at /status/ — the one this whole change exists to fix — the header would have read "II: (unresolved)" even with both instances discovered, and the canister line would have vanished. Rather than keep a back-compat single-instance shape alongside the new one, update the consumers. Both live in this repo and ship with the server, so there is no external reader to be compatible with, and a singular `iiOrigin` sitting next to `iiOrigins` reintroduces exactly the failure this PR removes: a consumer reads the singular field and silently reports one of N instances. - public/index.html renders every advertised II with its name and canister id. - facts.ii is renamed facts.iiInstances. The old key held ONE instance's facts, so anything still reaching for `facts.ii.canisterId` would read undefined against the new map; an absent key makes that a visible break rather than a quiet wrong answer. - report.test.js's evilReport now carries `iiOrigins`. That test had gone vacuous: it pins CWE-150 terminal-injection sanitising on a field the renderer no longer reads, so it passed while asserting nothing. Confirmed the coverage is real again by mutation — deleting the clean() call on the iiOrigins path fails both report tests, restoring it passes. Also drop the stale "/mcp → beta, /mcp-prod → production" examples from two comments I wrote before rebasing onto #92. /mcp-prod no longer exists; the repo now has no reference to it at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R8ZshwKmjD5fZ4Hs9dS6zh
…tname (#94) * Read the II pairing from /version instead of guessing it from the hostname The production dashboard reported "II /mcp connect page served: FAIL — GET https://internetcomputer.org/mcp → 404". Production was healthy; the check was probing the wrong host. The dashboard derived the Internet Identity origin by stripping the leading `mcp.` label off the MCP host. That encodes an assumption which only holds when the MCP server is a subdomain of its II: it maps mcp.beta.id.ai to beta.id.ai correctly, but maps mcp.internetcomputer.org to internetcomputer.org — the marketing site, which legitimately 404s on /mcp. A red II section on a healthy deployment is worse than no check, because it trains operators to ignore it. Fixing the value alone would have relocated the bug: one origin cannot describe a server that mounts more than one II, so whichever mount the hostname guess did not name went unmonitored everywhere, silently. So the server now advertises the pairing and the dashboard reads it: - GET /version gains an `instances` array — one entry per SERVED mount, with its name, mcp_path, ii_origin and ii_canister. Built from the live McpServer handles, so it tracks $MCP_SERVE_BETA and honours II_URL / II_URL_PROD overrides rather than restating defaults a monitor could drift from. McpServer::instance() exposes what it is built with. - The dashboard reads that array and runs the II-health section once per instance, ids and section titles suffixed per instance name. deriveIiOrigin is gone; no hostname arithmetic remains. - Advertised origins are still validated against the probe allowlist before anything is fetched — they arrive in a remote response, and a misconfigured or compromised server must not be able to steer these probes at a third party. A rejected entry is reported as a warn rather than dropped, so the section never reads as fully monitored while covering less. - A build with no instances[] (or an unreachable /version) now fails with "cannot determine the pairing" instead of confidently probing a guess. --ii / II_ORIGIN pins an origin for that case. deploy.sh pinned the dashboard's allowlist to the PARENT domain, because the derived II origin was a sibling host. On mcp.internetcomputer.org that quietly allowlisted all of internetcomputer.org. The II origins are now covered by the built-in id.ai suffixes, so it pins the deployment's own host instead. Verified end to end, not just by unit test: run against a local server with MCP_SERVE_BETA=1, the dashboard discovers both mounts and passes every check against the real id.ai and beta.id.ai; against live production (still running 80b8586, which predates instances[]) it now reports "advertises no II instances" instead of a false II outage, and --ii https://id.ai makes it pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R8ZshwKmjD5fZ4Hs9dS6zh * Update the web UI and report tests for the per-instance report shape Copilot caught a real break I missed: I updated the terminal renderer but not the hosted web UI. public/index.html read `targets.iiOrigin` and `facts.ii.canisterId`, so on the page Caddy actually serves at /status/ — the one this whole change exists to fix — the header would have read "II: (unresolved)" even with both instances discovered, and the canister line would have vanished. Rather than keep a back-compat single-instance shape alongside the new one, update the consumers. Both live in this repo and ship with the server, so there is no external reader to be compatible with, and a singular `iiOrigin` sitting next to `iiOrigins` reintroduces exactly the failure this PR removes: a consumer reads the singular field and silently reports one of N instances. - public/index.html renders every advertised II with its name and canister id. - facts.ii is renamed facts.iiInstances. The old key held ONE instance's facts, so anything still reaching for `facts.ii.canisterId` would read undefined against the new map; an absent key makes that a visible break rather than a quiet wrong answer. - report.test.js's evilReport now carries `iiOrigins`. That test had gone vacuous: it pins CWE-150 terminal-injection sanitising on a field the renderer no longer reads, so it passed while asserting nothing. Confirmed the coverage is real again by mutation — deleting the clean() call on the iiOrigins path fails both report tests, restoring it passes. Also drop the stale "/mcp → beta, /mcp-prod → production" examples from two comments I wrote before rebasing onto #92. /mcp-prod no longer exists; the repo now has no reference to it at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R8ZshwKmjD5fZ4Hs9dS6zh --------- Co-authored-by: Claude <noreply@anthropic.com>
PR #92 gated the beta II instance at /mcp-beta behind $MCP_SERVE_BETA (off by default), but no deployment set it, so /mcp-beta was never mounted anywhere (including staging). Wire it on for the staging environment only: - imcp2.service: add Environment=MCP_SERVE_BETA=__MCP_SERVE_BETA__, substituted by deploy.sh per environment. - deploy.sh: substitute __MCP_SERVE_BETA__ from $MCP_SERVE_BETA (default empty). - deploy-native.yml: pass MCP_SERVE_BETA=1 when inputs.environment == staging, empty otherwise, so production keeps serving /mcp (production II) alone. An empty value reads as off, so production's rendered unit is inert. Verified the unit renders MCP_SERVE_BETA=1 for staging and empty for production, and deploy.sh passes `bash -n`. Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym Co-authored-by: Claude <noreply@anthropic.com>
Summary
We now run separate production and staging deployments, each serving a single
/mcpendpoint. Previously the binary hardcoded two mounts on every deployment:/mcp(beta II) and/mcp-prod(production II). This makes/mcpthe production Internet Identity (id.ai) endpoint everywhere, removes/mcp-prod, and adds an opt-in/mcp-beta(beta II) that only the staging deployment turns on.mcp.internetcomputer.org): serves/mcpagainst production II./mcp-prodis gone./mcp(production II) plus/mcp-beta(beta II), enabled withMCP_SERVE_BETA.Related issues
No GitHub issue open; requested directly.
Changes
src/main.rs:/mcpnow usesIiInstance::prod()(production II,id.ai) and is the origin's default instance (owns the plain-root discovery documents)./mcp-prodremoved. A second instance, beta II at/mcp-beta, is composed only whenMCP_SERVE_BETAis truthy (1/true/yes/on); off by default so production serves/mcpalone. Router composition,auth_callbacks_router, the startup log line, and graceful-shutdown all handle the optional beta instance./version: reports the production gauge always and the beta gauge only when the staging instance is served (zero otherwise); keys are nowprod/beta.src/assets/index.html: the landing page lists/mcpas Production Internet Identity (dropped the/mcp-prodrow); the connector URL is nowhttps://mcp.internetcomputer.org/mcp.README.md: the Run/Auth sections describe/mcpas production II; the former "Production instance (/mcp-prod)" section is now "Staging: beta instance (/mcp-beta)", documenting theMCP_SERVE_BETAopt-in and itsII_URL/II_CANISTER_IDconfig./mcpconfig isII_URL_PROD/II_CANISTER_ID_PROD(defaultid.ai).tests/routers.rs: the composed test app now mirrors the staging shape (prod default at/mcp, beta at/mcp-beta); all assertions updated.src/lib.rs,src/identities.rs,src/auth.rs: doc comments and test fixtures that referenced/mcp-produpdated to the new wiring.Notes
/mcpusesIiInstance::prod(), whose default ishttps://id.ai(canisterrdmx6-jaaaa-aaaaa-aaadq-cai). TheII_URL_PROD/II_CANISTER_ID_PRODoverrides are retained for testing but default to production; no env is needed for prod. Say the word if you want a literal, non-overridable constant instead./mcp-prodonly completes once production II carries the #4086 MCP feature set" caveat was removed, since/mcpis now the primary production endpoint.Testing
cargo build --locked --all-targets(via the test build)cargo test --locked --all-targets— 136 lib + 7 router + 1 doctest passingcargo fmt --all/cargo clippy --all-targets— clippy is clean (no new warnings);cargo fmt --checkreports pre-existing repo-wide deviations across untouched files (a rustfmt-toolchain mismatch), so I did not reformat the treenpm test --prefix monitoring/mcp-status— not applicable (monitoring probes/mcpgenerically; no dashboard change)Checklist
Generated by Claude Code