Skip to content

Show friendly error screens for browser-facing sign-in/handshake failures - #74

Merged
aterga merged 8 commits into
mainfrom
claude/mcp-error-screens-0pyxtr
Jul 22, 2026
Merged

Show friendly error screens for browser-facing sign-in/handshake failures#74
aterga merged 8 commits into
mainfrom
claude/mcp-error-screens-0pyxtr

Conversation

@aterga

@aterga aterga commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

When a user hit an error during the MCP OAuth sign-in or the Internet Identity connect handshake, they reached these front-channel endpoints in a browser but were shown a raw JSON blob for every failure except the allow-list rejection. This makes those errors render as a nicely formatted, on-brand error screen that always offers the line "If this error is unexpected, please contact mcp@dfinity.org to report it." plus a best-effort diagnostic, matching the existing connect flow's look.

Related issues

Changes

  • Generalized the error-screen shell. src/assets/connect-error.html becomes a title/headline/detail/hint template rendered by a new error_screen helper. It reuses the connect flow's self-contained styling (parchment grid, editorial serif headline, foot-of-page "Hosted by" mark, light/dark theming) under the same strict, nonce'd, non-scripted, unframeable CSP that reflects no request value. not_allowlisted_page now builds on it.
  • /oauth/authorize errors now render friendly screens for browsers. Bad response_type, unknown client, and missing/unsupported PKCE go through a new signin_error helper that content-negotiates (accepts_html): a browser (Accept: text/html) gets the on-brand screen with a tailored diagnostic and the report-it line; a programmatic OAuth caller keeps the RFC-style JSON error. (Errors reached before a validated redirect_uri can't be redirected to the client, so they surface here.)
  • Handshake/redeem failures carry the contact line. The pinned connect-callback page gains a .contact-hint line that stays hidden during a normal connect and appears once the page enters its error state, so every handshake/redeem failure the user lands on shows the contact.
  • Renamed ALLOWLIST_CONTACTCONTACT (one address for every sign-in path) and updated the module/README docs. Back-channel endpoints (/oauth/token, /oauth/register, the /mcp bearer gate) stay JSON — no browser ever lands on them.
  • Updated and extended the auth.rs tests for the new content negotiation, the generalized template, and the callback contact line.

Screens were rendered from the real assets and eyeballed in light and dark themes (authorize error, allow-list rejection, callback error state, and the normal loading state confirming the contact line stays hidden).

Testing

  • cargo build --locked --all-targets
  • cargo test --locked --all-targets (130 passed)
  • cargo fmt --all / cargo clippy --all-targets (tree is warning-free) — repo is not rustfmt-clean and carries two pre-existing dead-code warnings unrelated to this change; new code follows the surrounding style
  • npm test --prefix monitoring/mcp-status (dashboard unchanged)

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 with Claude Code

https://claude.ai/code/session_015Pw6dN2imxtGQfpujcFFeG


Generated by Claude Code

…ures

Users who hit an error during the OAuth sign-in or the Internet Identity
connect handshake reached these front-channel endpoints in a browser but
saw a raw JSON blob for every failure except the allow-list rejection.

Render an on-brand error screen instead, reusing the connect flow's
self-contained shell (parchment grid, serif headline, "Hosted by" mark,
light/dark theming, strict nonce'd non-scripted unframeable CSP):

- Generalize assets/connect-error.html into a title/headline/detail/hint
  template rendered by a new `error_screen` helper; `not_allowlisted_page`
  now builds on it.
- `/oauth/authorize` errors (bad response_type, unknown client, missing or
  unsupported PKCE) now content-negotiate: a browser gets the screen with a
  best-effort diagnostic and the line "If this error is unexpected, please
  contact mcp@dfinity.org to report it."; a programmatic OAuth caller keeps
  the RFC-style JSON error.
- The pinned connect-callback page gains a contact/report line that stays
  hidden during a normal connect and appears once a handshake/redeem failure
  puts it in its error state, so every handshake error carries the contact.

Rename ALLOWLIST_CONTACT to CONTACT (one address for every sign-in path).
Back-channel endpoints (/oauth/token, /oauth/register, the /mcp bearer gate)
stay JSON — no browser ever lands on them. Tests updated for the new
content negotiation; docs note the behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Pw6dN2imxtGQfpujcFFeG

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 improves the UX of MCP sign-in/connect front-channel failures by replacing raw JSON error blobs with consistent, on-brand HTML error screens (while preserving RFC-style JSON for programmatic OAuth callers via content negotiation).

Changes:

  • Generalizes connect-error.html into a reusable title/headline/detail/hint template rendered via a new error_screen helper.
  • Adds HTML-vs-JSON content negotiation for /oauth/authorize failures via accepts_html + signin_error, keeping back-channel endpoints JSON-only.
  • Updates the pinned connect callback page + shared CSS to include a hidden “contact us to report it” line that appears when the page enters an error state.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/auth.rs Adds generalized error-screen rendering + /oauth/authorize content negotiation and updates callback page templating/tests.
src/assets/connect.css Adds styling to hide/show the callback page contact hint only in error state.
src/assets/connect-error.html Converts the error page into a placeholder-driven template for error_screen.
src/assets/connect-callback.html Adds a contact-hint line (mailto) that is revealed on callback error state.
README.md Documents the new browser-facing error screen behavior and negotiation rules.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/auth.rs
The content-negotiation check was a case-sensitive substring match for
"text/html" that ignored quality values, so `Accept: text/html;q=0,
application/json` (an explicit "not acceptable") would still be served the
HTML error screen, and `TEXT/HTML` would be missed. Parse the header into
media ranges instead: match the media type case-insensitively (RFC 9110
§12.5.1) and treat an explicit `;q=0` as not acceptable (§12.4.2). Wildcards
still don't opt into HTML — only an explicit, acceptable text/html does —
so the machine default stays JSON.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Pw6dN2imxtGQfpujcFFeG
@aterga
aterga marked this pull request as ready for review July 22, 2026 15:06
Copilot AI review requested due to automatic review settings July 22, 2026 15:06

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 5 out of 5 changed files in this pull request and generated no new comments.

Make the sign-in/handshake error screens wrap more elegantly: headlines
use text-wrap: balance (with a little more width) so a line never leaves a
lone trailing word, and body copy uses text-wrap: pretty to avoid orphan
words, with slightly roomier line-height. Both degrade gracefully where the
property is unsupported.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Pw6dN2imxtGQfpujcFFeG
Copilot AI review requested due to automatic review settings July 22, 2026 15:16

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 5 out of 5 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/auth.rs:859

  • The doc comment says the request "prefers" HTML, but accepts_html only checks whether text/html is explicitly acceptable (it does not compare q-values vs JSON). This mismatch can mislead future readers about the intended negotiation semantics.
/// Whether the request prefers an HTML response — i.e. it comes from a browser.
/// `/oauth/authorize` is a front-channel endpoint reached by top-level navigation,
/// so a browser (which always sends `Accept: text/html`) gets the friendly
/// [`error_screen`], while a programmatic OAuth caller (`Accept: application/json`,
/// `*/*`, or no `Accept` at all) keeps the machine-readable JSON. Keying on an

Comment thread src/auth.rs Outdated
…ication

Refine the browser-facing sign-in and handshake copy per request:
- No em or en dashes anywhere in the screens; long sentences are split into
  short ones that wrap into balanced lines.
- Callback handshake failures now read as proper capitalized sentences
  instead of lowercase fragments (they render as the on-screen headline).
- Warmer, plainer wording ("We couldn't..."); the report line now reads
  "please email mcp@dfinity.org to report it".

Also address PR review feedback:
- accepts_html: the doc comment now states it is a coarse browser-vs-machine
  accept check, not a full q-value preference ranking (it already honors q=0).
- authorize: a malformed or ineligible redirect_uri (unparseable, non-https,
  or userinfo-bearing) is now classified invalid_request and shown the generic
  sign-in error, rather than the misleading "request access" allow-list page,
  which is meant only for a well-formed hosted redirect that isn't approved
  yet. Adds is_wellformed_hosted_redirect and a covering test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Pw6dN2imxtGQfpujcFFeG
Copilot AI review requested due to automatic review settings July 22, 2026 15:29

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 5 out of 5 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/auth.rs:274

  • is_wellformed_hosted_redirect currently treats https://localhost/... / https://127.0.0.1/... / https://[::1]/... as “well‑formed hosted”. Those redirects are not permitted by redirect_uri_permitted (loopback is http-only here), so /oauth/authorize will misclassify them as an allow‑list approval gap and show the “request access” screen instead of an invalid_request/generic sign-in error.
        Ok(url) => {
            url.scheme() == "https"
                && url.username().is_empty()
                && url.password().is_none()
                && url.host_str().is_some_and(|h| !h.is_empty())

- End the error headlines with a full stop, so every headline reads as a
  complete sentence (the callback headlines already did).
- Make the allow-list request actionable: instead of asking for the redirect
  URI (which a user has no way to find), ask them to tell us the name of
  their MCP client or AI chatbot, which the operator can map to the vendor.
- Give the malformed-handshake message an explicit next step ("Restart from
  your client."), so that screen is actionable too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Pw6dN2imxtGQfpujcFFeG
Copilot AI review requested due to automatic review settings July 22, 2026 21:18

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 5 out of 5 changed files in this pull request and generated 3 comments.

Comment thread src/auth.rs
Comment thread src/auth.rs Outdated
Comment thread src/assets/connect-callback.html Outdated
- Restore "please contact mcp@dfinity.org to report it" in the error screens
  and the callback page, matching the README/PR wording and the original
  request (the copy pass had drifted it to "please email").
- Reword the machine-facing invalid_request description for a malformed
  redirect_uri to "redirect_uri must be a valid https or loopback URL",
  dropping the awkward "an https" phrasing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Pw6dN2imxtGQfpujcFFeG
Copilot AI review requested due to automatic review settings July 22, 2026 21:22

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 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread src/assets/connect-callback.html
Comment thread README.md Outdated
claude added 2 commits July 22, 2026 21:28
- Wrap the callback status message and its contact line in one role=status
  (polite, atomic) live region, so a screen reader is told the error AND the
  "please contact ... to report it" line together when the page enters its
  error state, instead of only the error. A .status flex wrapper preserves the
  visual spacing.
- README: reference the stylesheet by its real path, src/assets/connect.css.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Pw6dN2imxtGQfpujcFFeG
Copilot AI review requested due to automatic review settings July 22, 2026 21:32

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 5 out of 5 changed files in this pull request and generated no new comments.

@aterga
aterga merged commit d80e6d8 into main Jul 22, 2026
2 checks passed
aterga pushed a commit that referenced this pull request Jul 23, 2026
Follow-up reconciliation after #26 (library crate), #70, #73, #74 landed on
main. Verified against the current source; three concrete fixes:

- Deploy: the connect/landing HTML/CSS/SVG live in `src/assets/` (compiled in
  via include_str! from src/main.rs and src/auth.rs), not `static/`; `static/`
  holds only the reference docs. The old text conflated the two and contradicted
  the "Browser-facing error screens" section. Reframed as the deployment binary
  being self-contained, with both dirs as build-time inputs.
- Intro: ic-agent pin is 0.49 (Cargo.toml), not 0.48.
- Tools table: `canister_query` and `canister_update_call` take an optional
  `candid` arg (the .did fallback when a canister publishes no candid:service
  metadata); added it to both rows.

A wider audit found the rest of the doc already matches main (tool set,
endpoints, OAuth path-pinned allow-list, env vars, II methods, library API).

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
Follow-up reconciliation after #26 (library crate), #70, #73, #74 landed on
main. Verified against the current source; three concrete fixes:

- Deploy: the connect/landing HTML/CSS/SVG live in `src/assets/` (compiled in
  via include_str! from src/main.rs and src/auth.rs), not `static/`; `static/`
  holds only the reference docs. The old text conflated the two and contradicted
  the "Browser-facing error screens" section. Reframed as the deployment binary
  being self-contained, with both dirs as build-time inputs.
- Intro: ic-agent pin is 0.49 (Cargo.toml), not 0.48.
- Tools table: `canister_query` and `canister_update_call` take an optional
  `candid` arg (the .did fallback when a canister publishes no candid:service
  metadata); added it to both rows.

A wider audit found the rest of the doc already matches main (tool set,
endpoints, OAuth path-pinned allow-list, env vars, II methods, library API).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
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