Skip to content

Serve production II at /mcp; drop /mcp-prod; add opt-in /mcp-beta for staging - #92

Merged
aterga merged 3 commits into
mainfrom
claude/prod-mcp-endpoint
Jul 28, 2026
Merged

Serve production II at /mcp; drop /mcp-prod; add opt-in /mcp-beta for staging#92
aterga merged 3 commits into
mainfrom
claude/prod-mcp-endpoint

Conversation

@aterga

@aterga aterga commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

We now run separate production and staging deployments, each serving a single /mcp endpoint. Previously the binary hardcoded two mounts on every deployment: /mcp (beta II) and /mcp-prod (production II). This makes /mcp the 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.

  • Production (mcp.internetcomputer.org): serves /mcp against production II. /mcp-prod is gone.
  • Staging: serves the same /mcp (production II) plus /mcp-beta (beta II), enabled with MCP_SERVE_BETA.

Related issues

No GitHub issue open; requested directly.

Changes

  • src/main.rs: /mcp now uses IiInstance::prod() (production II, id.ai) and is the origin's default instance (owns the plain-root discovery documents). /mcp-prod removed. A second instance, beta II at /mcp-beta, is composed only when MCP_SERVE_BETA is truthy (1/true/yes/on); off by default so production serves /mcp alone. 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 now prod/beta.
  • src/assets/index.html: the landing page lists /mcp as Production Internet Identity (dropped the /mcp-prod row); the connector URL is now https://mcp.internetcomputer.org/mcp.
  • README.md: the Run/Auth sections describe /mcp as production II; the former "Production instance (/mcp-prod)" section is now "Staging: beta instance (/mcp-beta)", documenting the MCP_SERVE_BETA opt-in and its II_URL/II_CANISTER_ID config. /mcp config is II_URL_PROD/II_CANISTER_ID_PROD (default id.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-prod updated to the new wiring.

Notes

  • "Hardcode id.ai": /mcp uses IiInstance::prod(), whose default is https://id.ai (canister rdmx6-jaaaa-aaaaa-aaadq-cai). The II_URL_PROD/II_CANISTER_ID_PROD overrides 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.
  • The stale "/mcp-prod only completes once production II carries the #4086 MCP feature set" caveat was removed, since /mcp is 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 passing
  • cargo fmt --all / cargo clippy --all-targets — clippy is clean (no new warnings); cargo fmt --check reports pre-existing repo-wide deviations across untouched files (a rustfmt-toolchain mismatch), so I did not reformat the tree
  • npm test --prefix monitoring/mcp-status — not applicable (monitoring probes /mcp generically; no dashboard change)

Checklist

  • I have read the Contributing guidelines.
  • Docs (README / comments) updated for any user-visible change.
  • No secrets, credentials, or internal-only information are included.

Generated by Claude Code

… 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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 /mcp to IiInstance::prod() (production II) and make it the default instance that owns the plain-root discovery documents.
  • Remove /mcp-prod and add optional /mcp-beta (beta II) behind MCP_SERVE_BETA, including correct router composition, logging, shutdown, and /version gauges.
  • 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.

Comment thread tests/routers.rs
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
@aterga
aterga marked this pull request as ready for review July 28, 2026 16:05
@aterga
aterga requested a review from a team as a code owner July 28, 2026 16:05
Copilot AI review requested due to automatic review settings July 28, 2026 16:05
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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`.

Copilot AI review requested due to automatic review settings July 28, 2026 16:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 /mcp uses 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 correct II_URL_PROD/II_CANISTER_ID_PROD config.
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_DEFAULT to 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";

@aterga
aterga merged commit 79ef0c7 into main Jul 28, 2026
2 checks passed
aterga pushed a commit that referenced this pull request Jul 28, 2026
…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
aterga pushed a commit that referenced this pull request Jul 28, 2026
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
aterga added a commit that referenced this pull request Jul 28, 2026
…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>
aterga added a commit that referenced this pull request Jul 29, 2026
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>
@aterga aterga mentioned this pull request Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants