Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions monitoring/mcp-status/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,27 @@ 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.<env>.id.ai` ↔ `<env>.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` 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
Expand Down Expand Up @@ -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.
72 changes: 27 additions & 45 deletions monitoring/mcp-status/checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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",
Expand Down Expand Up @@ -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",
label: "II /mcp connect page served",
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`,
});
}

Expand Down Expand Up @@ -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(
Expand Down
15 changes: 9 additions & 6 deletions monitoring/mcp-status/checks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -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:*`,
Expand All @@ -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();
Expand Down