From 779c818327c370e86f2789cc725f40b4dfee332c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 20:00:19 +0000 Subject: [PATCH 1/2] /status: check II /mcp is served, not form-action https: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "II /mcp delegation flow enabled" check WARNed because it asserted the /mcp CSP form-action allows https:. That was the precondition for the old delegation flow, where II form-POSTed the delegation to the server. II #4086 retired that flow, so form-action no longer allows https: (it's now 'self' http://127.0.0.1:*, tightened on purpose); the 127.0.0.1 entry is for the unrelated /cli loopback flow, not MCP. The current connect flow never form-POSTs: the key-request and completion callbacks are fetch() calls (governed by connect-src, which allows the https origin) and the hand-back to finish_url is a top-level navigation (not governed by form-action). Nothing is blocked — the check was a false positive. Fix (the ticket's preferred option): drop the form-action assertion and just verify GET /mcp returns 200. Per-user trust is on-chain and not globally inspectable, so a served connect page is the only meaningful instance-wide signal. Updates the check description/expected/detail, drops the now-dead WARN suggestion, and refreshes the dashboard README. Replaces the obsolete "warns on loopback-only form-action" test with one pinning that a served /mcp now passes regardless of form-action. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym --- monitoring/mcp-status/README.md | 25 ++++++----- monitoring/mcp-status/checks.js | 66 ++++++++++------------------ monitoring/mcp-status/checks.test.js | 15 ++++--- 3 files changed, 48 insertions(+), 58 deletions(-) diff --git a/monitoring/mcp-status/README.md b/monitoring/mcp-status/README.md index c2808d3..8523813 100644 --- a/monitoring/mcp-status/README.md +++ b/monitoring/mcp-status/README.md @@ -20,16 +20,21 @@ It answers three questions and adds a few suggestions: 3. **Is that II instance healthy and is its `/mcp` delegation flow enabled?** — checks the II frontend is reachable and IC-certified, reports its frontend canister id and related origins, confirms it serves its runtime config - (textual Candid) at `/.config`, and verifies the `/mcp` delegation page is - served with a CSP `form-action` relaxed to allow posting the delegation - callback to an https MCP server (`'self' https:` on `/mcp` paths, vs the - tighter `'self' http://127.0.0.1:*` SPA-wide). Since + (textual Candid) at `/.config`, and verifies the `/mcp` connect page is + served. The connect flow runs on `fetch()` callbacks (governed by CSP + `connect-src`, which allows the https MCP origin) and a top-level navigation + back to the server's `finish_url` — neither is gated by `form-action` — so a + served page is the health signal. (The old delegation flow form-POSTed the + callback, which needed a relaxed `form-action`; that flow was retired in + [internet-identity#4086](https://github.com/dfinity/internet-identity/pull/4086), + and the `'self' http://127.0.0.1:*` `form-action` now on `/mcp` is for the + unrelated `/cli` loopback flow.) Since [internet-identity#4052](https://github.com/dfinity/internet-identity/pull/4052) there is no global `mcp_server_origin` and **trust is per-user**: each identity adds the MCP server it trusts in II Settings, synced on-chain. So whether a *specific* identity trusts this server is not inspectable from here without authenticating — what is instance-wide and checkable is that the - delegation flow itself is deployed and its callback POST is permitted. + connect page itself is deployed. Every check carries a plain-language description, and the report shows which **deployment is running** — the MCP server's version and git commit (read from @@ -119,8 +124,8 @@ actually matters. Against `https://mcp.beta.id.ai` the server passes all endpoint checks; the linked II is `https://beta.id.ai` (frontend canister `gjxif-ryaaa-aaaad-ae4ka-cai`, -backend `fgte5-ciaaa-aaaad-aaatq-cai`), whose `/mcp` delegation flow is enabled -(its `/mcp` CSP `form-action` is relaxed to `'self' https:`, so the connect -callback can post back). Whether a given identity trusts this MCP server is now -per-user (set in II Settings, synced on-chain) and so is not asserted here. With -the linked II healthy, the dashboard reports all green. +backend `fgte5-ciaaa-aaaad-aaatq-cai`), whose `/mcp` connect page is served (the +connect flow is `fetch()`/navigation-based, so its CSP is not asserted). Whether +a given identity trusts this MCP server is now per-user (set in II Settings, +synced on-chain) and so is not asserted here. With the linked II healthy, the +dashboard reports all green. diff --git a/monitoring/mcp-status/checks.js b/monitoring/mcp-status/checks.js index 5c59e50..1c1c264 100644 --- a/monitoring/mcp-status/checks.js +++ b/monitoring/mcp-status/checks.js @@ -661,50 +661,41 @@ export const checkIiHealth = async (iiOrigin, mcpOrigin, timeoutMs) => { : "no ic-certificate header (response not certified by the IC?)", }); - // 3. /mcp delegation flow. Since dfinity/internet-identity#4052 the II no - // longer has a global `mcp_server_origin`, and trust is per-user: each - // identity adds the MCP server it trusts in II Settings, synced on-chain. - // So there is no global, unauthenticated signal that names this specific - // server — recognition is per-identity and not inspectable from here. - // What IS instance-wide and inspectable is whether the `/mcp` connect page - // is served and whether its CSP `form-action` is relaxed to allow posting - // the delegation callback to an https MCP server (`'self' https:` on /mcp - // paths, vs the tighter `'self' http://127.0.0.1:*` SPA-wide). That relaxed - // form-action is what lets any https MCP server's connect callback POST - // back, so it is the authoritative health signal for the delegation flow. + // 3. /mcp connect page. Since dfinity/internet-identity#4052 the II no longer + // has a global `mcp_server_origin`, and trust is per-user: each identity + // adds the MCP server it trusts in II Settings, synced on-chain. So there is + // no global, unauthenticated signal that names this specific server — + // recognition is per-identity and not inspectable from here. + // + // There is also no instance-wide CSP signal to assert. The old delegation + // flow form-POSTed the callback to the server, so a relaxed `form-action` + // (`'self' https:`) was its precondition — but dfinity/internet-identity + // #4086 retired that flow. The current connect flow never form-POSTs: the + // key-request and completion callbacks are `fetch()` calls (governed by + // `connect-src`, which allows the https origin) and the hand-back to + // `finish_url` is a top-level navigation (not governed by `form-action`). + // The tightened `form-action 'self' http://127.0.0.1:*` now on /mcp is for + // the unrelated /cli loopback flow, not MCP. So the only meaningful + // instance-wide health signal left is that the /mcp connect page is served. { const url = `${iiOrigin}/mcp`; const mr = await probe(url, { timeoutMs }); - const mcpFormAction = parseCspDirective( - mr.headers.get("content-security-policy"), - "form-action", - ); - facts.mcpFormAction = mcpFormAction; - // The callback is posted to an https origin, so the /mcp page's form-action - // must permit https: (the `https:` scheme source, or this exact origin). - const allowsHttpsPost = - !!mcpFormAction && - (mcpFormAction.includes("https:") || mcpFormAction.includes(mcpOrigin)); const served = mr.ok && mr.status === 200; checks.push({ id: "ii-mcp-flow", label: "II /mcp delegation flow enabled", description: - "Confirms the II serves its /mcp delegation page and that its CSP form-action is relaxed to allow posting the delegation callback to an https MCP server. Since #4052 trust is per-user (each identity adds its trusted server in II Settings, synced on-chain), so which servers a given identity trusts is not globally inspectable; this checks the instance-wide flow is enabled.", - target: `GET ${url} CSP form-action`, - expected: "200 and form-action allows https: (callback can post to https)", - status: served ? (allowsHttpsPost ? "pass" : "warn") : "fail", + "Confirms the II serves its /mcp connect page. The connect flow runs on fetch() callbacks (governed by CSP connect-src, which allows the https MCP origin) and a top-level navigation back to the server's finish_url — neither is gated by form-action — so serving the page is the health signal. Since #4052 trust is per-user (each identity adds its trusted server in II Settings, synced on-chain), which servers a given identity trusts is not globally inspectable; this checks the instance-wide flow is enabled.", + target: `GET ${url}`, + expected: "200 (connect page served)", + status: served ? "pass" : "fail", httpStatus: mr.status, latencyMs: mr.latencyMs, detail: mr.error ? `request failed: ${mr.error.message}` - : !served - ? `${mr.status}, /mcp delegation page not served` - : mcpFormAction - ? allowsHttpsPost - ? `form-action = [${mcpFormAction.join(", ")}] → can post the callback to https MCP servers` - : `form-action = [${mcpFormAction.join(", ")}] (does NOT allow https: — the callback to ${mcpOrigin} would be blocked)` - : "200 but no form-action directive in the /mcp CSP", + : served + ? `${mr.status}, /mcp connect page served` + : `${mr.status}, /mcp connect page not served`, }); } @@ -840,19 +831,10 @@ export const buildSuggestions = (sections, facts) => { if (checkById["ii-mcp-flow"]?.status === "fail") { suggestions.push( - "The linked II does not serve its /mcp delegation page. The MCP connect " + + "The linked II does not serve its /mcp connect page. The MCP connect " + "flow cannot run until an II build that includes the /mcp flow is " + "deployed at this origin.", ); - } else if (checkById["ii-mcp-flow"]?.status === "warn") { - suggestions.push( - "The II /mcp page is served but its CSP form-action does not allow " + - "https:, so it cannot post the delegation callback to an https MCP " + - "server. Note that since #4052 each user also adds the trusted MCP " + - "server in II Settings (synced on-chain): a connect only succeeds for " + - "servers the signed-in identity has trusted, which this dashboard " + - "cannot verify without authenticating.", - ); } if (checkById["mcp-challenge"]?.status !== "pass") { suggestions.push( diff --git a/monitoring/mcp-status/checks.test.js b/monitoring/mcp-status/checks.test.js index 1ac1988..f1e5383 100644 --- a/monitoring/mcp-status/checks.test.js +++ b/monitoring/mcp-status/checks.test.js @@ -255,10 +255,11 @@ test("checkIiHealth verifies the /mcp delegation flow and config", async () => { "content-security-policy": `default-src 'none';form-action 'self' http://127.0.0.1:*;frame-ancestors 'self' ${ii} https://beta.identity.ic0.app`, }, }), - // The /mcp connect page relaxes form-action to allow https: callback posts. + // The /mcp connect page is served (200) — that is the whole health signal; + // the connect flow is fetch/navigation-based, so its CSP is not asserted. [`GET ${ii}/mcp`]: resp(200, { headers: { - "content-security-policy": `default-src 'none';form-action 'self' https:`, + "content-security-policy": `default-src 'none';form-action 'self' http://127.0.0.1:*`, }, }), [`GET ${ii}/.config`]: resp(200, { @@ -305,15 +306,17 @@ test("checkIiHealth fails when the /mcp flow is not served", async () => { } }); -test("checkIiHealth warns when the /mcp form-action forbids https posts", async () => { +test("checkIiHealth passes on a served /mcp even with loopback-only form-action", async () => { const ii = "https://beta.test"; const mcp = "https://mcp.beta.test"; const restore = stubFetch({ [`GET ${ii}/`]: resp(200, { headers: { "content-security-policy": `form-action 'self'` }, }), - // Page served, but form-action only allows loopback — an https callback POST - // would be blocked, so the flow is reachable but misconfigured for remotes. + // /mcp is served but its form-action only allows loopback (that entry is for + // the unrelated /cli flow). Since II #4086 the MCP connect flow uses fetch() + // (connect-src) + a top-level navigation to finish_url, not a form POST, so a + // tightened form-action must NOT warn — a served page is healthy. [`GET ${ii}/mcp`]: resp(200, { headers: { "content-security-policy": `form-action 'self' http://127.0.0.1:*`, @@ -326,7 +329,7 @@ test("checkIiHealth warns when the /mcp form-action forbids https posts", async }); try { const { section } = await checkIiHealth(ii, mcp, 2000); - assert.equal(byId(section, "ii-mcp-flow").status, "warn"); + assert.equal(byId(section, "ii-mcp-flow").status, "pass"); assert.equal(byId(section, "ii-config").status, "pass"); } finally { restore(); From 9d2fe8105f3b5bcc7c7df8f1d86569728183674e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 20:17:11 +0000 Subject: [PATCH 2/2] /status: rename ii-mcp-flow label + docs to "connect page served" Follow-up to the review: the check now only asserts GET /mcp returns 200, so "delegation flow enabled" was misleading. Rename the user-facing label to "II /mcp connect page served" (the `ii-mcp-flow` id is unchanged) and align the remaining "delegation flow enabled" phrasings in the linkage description and README (heading + question 2) with the connect-page terminology. The historical reference to the old form-POST delegation flow is left intact. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym --- monitoring/mcp-status/README.md | 4 ++-- monitoring/mcp-status/checks.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/monitoring/mcp-status/README.md b/monitoring/mcp-status/README.md index 8523813..36c0255 100644 --- a/monitoring/mcp-status/README.md +++ b/monitoring/mcp-status/README.md @@ -14,10 +14,10 @@ It answers three questions and adds a few suggestions: 2. **Which Internet Identity instance is it linked to?** — resolves the II origin (derived from the `mcp..id.ai` ↔ `.id.ai` convention, or overridden explicitly). Question 3 then checks that resolved origin is - reachable and has its `/mcp` delegation flow enabled. The pairing itself is + reachable and serving its `/mcp` connect page. The pairing itself is inferred from config / the naming convention — the dashboard doesn't live-verify that the MCP server actually hands off to that II instance. -3. **Is that II instance healthy and is its `/mcp` delegation flow enabled?** — +3. **Is that II instance healthy and is its `/mcp` connect page served?** — checks the II frontend is reachable and IC-certified, reports its frontend canister id and related origins, confirms it serves its runtime config (textual Candid) at `/.config`, and verifies the `/mcp` connect page is diff --git a/monitoring/mcp-status/checks.js b/monitoring/mcp-status/checks.js index 1c1c264..2b66024 100644 --- a/monitoring/mcp-status/checks.js +++ b/monitoring/mcp-status/checks.js @@ -532,7 +532,7 @@ export const checkMcpEndpoints = async (mcpOrigin, timeoutMs) => { * redirects — so there is no `Location` header to read and the probe could * never pass. Instead we resolve the origin (configured or naming-convention * derived); the II-health section below then checks that *resolved* origin is - * reachable and has its `/mcp` delegation flow enabled. Note this does not + * reachable and serving its `/mcp` connect page. Note this does not * live-verify that the MCP server actually hands off to that II instance — the * pairing is inferred from config / the naming convention, not proven. * @@ -559,7 +559,7 @@ export const checkLinkage = (mcpOrigin, configuredIi, iiOriginSource) => { id: "ii-target", label: "Linked Internet Identity instance", description: - "Identifies which Internet Identity instance this MCP server is paired with (explicitly configured, or derived from the naming convention — the pairing is inferred, not live-verified). The II-health section below then checks that resolved origin is reachable and has its /mcp delegation flow enabled.", + "Identifies which Internet Identity instance this MCP server is paired with (explicitly configured, or derived from the naming convention — the pairing is inferred, not live-verified). The II-health section below then checks that resolved origin is reachable and serving its /mcp connect page.", target: mcpOrigin, expected: "a resolvable II origin", status: iiOrigin ? "pass" : "fail", @@ -683,7 +683,7 @@ export const checkIiHealth = async (iiOrigin, mcpOrigin, timeoutMs) => { const served = mr.ok && mr.status === 200; checks.push({ id: "ii-mcp-flow", - label: "II /mcp delegation flow enabled", + label: "II /mcp connect page served", description: "Confirms the II serves its /mcp connect page. The connect flow runs on fetch() callbacks (governed by CSP connect-src, which allows the https MCP origin) and a top-level navigation back to the server's finish_url — neither is gated by form-action — so serving the page is the health signal. Since #4052 trust is per-user (each identity adds its trusted server in II Settings, synced on-chain), which servers a given identity trusts is not globally inspectable; this checks the instance-wide flow is enabled.", target: `GET ${url}`,