On this page
1 · The two logins a doctor can arrive at2 · What arrives when Claude sends a doctor to log in3 · Finishing the flow instead of landing on the dashboard4 · The open-redirect guard actually rejects a hostile addressWhat we couldn't screenshot, and whyWalkthrough
Everything below was captured live from the feat-claude-mcp sandbox (https://feat-claude-mcp.sandbox.anastawfik.com). Two things about that sandbox matter before looking at the screenshots:
The MCP server itself is not running anywhere reachable, including this sandbox. The sandbox has noOAUTH_ISSUER,OAUTH_MCP_RESOURCE, orOAUTH_CLINIC_WEB_LOGIN_URLenvironment variables set, andpackages/server/src/index.js:436only mounts the/oauth/*routes when those three are present. We confirmed this directly: every/oauth/*and/.well-known/oauth-authorization-serverrequest against the sandbox's API falls through to the GraphQL handler and returns a GraphQL error, not an OAuth response. So there is no "Connect" button to click yet, and no consent screen to screenshot on this environment — that part of the PR is proven only by its test suite (see For Quality) and by the design document, not by a live click-through. This is worth flagging as a gap: the PR description says the sandbox "exercises the OAuth server over real HTTPS on a real domain," which is true of the code path being deployable, but not of this particular sandbox instance, which is missing the configuration to turn it on.
What is live and real on this sandbox is the pair of existing clinic-web login screens this PR modifies, and the client-side logic (oauthReturn.js) that decides where a doctor lands after signing in. That logic doesn't need the OAuth server running to be exercised — it only needs a URL carrying an oauth_return parameter, which is exactly what the real OAuth server would hand it once deployed. So every screenshot below is of the real, deployed code, exercised the same way the finished OAuth flow will exercise it.
1 · The two logins a doctor can arrive at
Dentolize has always had two ways to sign in to clinic-web. Both had to be taught to finish an OAuth connection, because a doctor connecting Claude might land on either one.
Email + password (/auth/login):

Username + clinic name (/auth/loginCompany) — used by clinics that log in by username rather than email. Step 1 asks for the clinic's login name:

Step 2 asks for the username and password:

2 · What arrives when Claude sends a doctor to log in
When the real OAuth server sends a doctor's browser to clinic-web, it appends ?oauth_return=<our callback URL> to the login link. Here's the email login screen with that parameter present — notice the "Or login with username" link at the bottom has picked up the same parameter (oauthLink() in oauthReturn.js:104), so switching login screens mid-flow doesn't lose the doctor's place:

3 · Finishing the flow instead of landing on the dashboard
This is the actual behavior change. Before this PR, finishing login always sent a doctor to the dashboard. Now, if an oauth_return address is present and it points somewhere trustworthy, login sends them there instead.
We proved this two ways, both against the real deployed sandbox:
Email login, with oauth_return pointing at a same-origin address (/settings) — after submitting real sandbox credentials, the browser lands on /settings, not the dashboard:

Username + clinic-name login, same test — this path matters separately because, per BUGS.md §7, this screen navigates imperatively right after the login mutation resolves, before React ever gets a chance to re-render and run a guard. A guard placed only in the render path (the first fix attempted) never ran here, and the connection silently failed with no error message. This screenshot is proof the fixed version — a guard placed in the submit handler itself — actually redirects on this screen too:

4 · The open-redirect guard actually rejects a hostile address
goToOAuthReturn() only follows the oauth_return address if it parses to one of two trusted origins (the clinic-web app itself, or the API). We tested it with https://feat-claude-mcp.sandbox.anastawfik.com.attacker.example/steal — a string that starts with the real hostname but is a completely different site once parsed as a URL. Logging in with that parameter present lands back on the ordinary dashboard, exactly as it would with no oauth_return at all — the guard silently declines to follow it rather than erroring, which is the correct behavior (a doctor should never see security plumbing):

What we couldn't screenshot, and why
The consent screen, the clinic chooser, and the "Connect" experience inside Claude all exist only as server-rendered HTML in packages/server/src/apis/oauth/consentPage.js and as test assertions in router.test.js — not as anything reachable on this sandbox. See Connecting Claude: the OAuth login flow for what that code does, verified by reading it directly, and For Quality for what the 259 server-side OAuth tests actually assert about it.