Commit 64b3544
authored
Self-host MCP OAuth: connect-card path, approval screen, real-agent e2e + framework dwells (#1002)
* Self-host: serve the org-scoped MCP path the Connect card prints
The console Connect-an-agent card prints `<origin>/<organizationId>/mcp` —
a convention the multi-tenant cloud worker routes by stripping the org segment
at the edge. Self-host only served the bare `/mcp`, so a connecting MCP client
completed OAuth and then 404'd on reconnect. Self-host is single-tenant (the
session pins the one org), so the segment carries no routing meaning: both
front-ends (the prod Bun server in serve.ts and the vite dev middleware) now
strip a single leading segment from `/<seg>/mcp` and the matching OAuth
discovery path, mirroring cloud's edge rewrite but accepting any segment.
Coverage gap this slipped through: every MCP e2e built `${baseUrl}/mcp`
itself and connected to that, so none exercised the URL the card actually
hands the user. New scenario selfhost/mcp-connect-card-url scrapes the URL out
of the rendered card and asserts the server serves it (was 404, now 401) — it
fails on the pre-fix build. Also adds selfhost/mcp-oauth-consent, which records
the MCP OAuth browser flow and documents that it currently auto-consents (no
approval screen).
Shared pure helper (stripMcpOrgSegment) with unit tests; verified green on
both the selfhost (vite) and selfhost-docker (production image) targets.
* Self-host: require an MCP OAuth approval screen
Connecting MCP clients were auto-consented — a signed-in user's client got a
token with no prompt. Better Auth's MCP authorize only shows consent when the
request carries prompt=consent (clients don't send it) AND a consentPage is
configured. So: configure oidcConfig.consentPage -> the SPA route
/mcp-consent, and force the screen by injecting prompt=consent on every
/api/auth/mcp/authorize in the shared Better Auth handler (force-mcp-consent,
unit-tested). The handler also looks the registered client_name up from the
DCR row and appends it to the consent redirect so the screen reads
"Connect <name>?" rather than an opaque id (self-declared — a label, not a
trust signal). The /mcp-consent page (rebuilt from the earlier
test/selfhost-e2e-org-setup work) shows the client, scopes, an explicit note
that the grant is limited to the MCP server (not a web-app login, no other API
calls), and Allow/Deny posting to /api/auth/oauth2/consent.
Also fixes a dev-only routing bug the new route exposed: the vite middleware
matched MCP paths with rawUrl.startsWith("/mcp"), which swallowed
/mcp-consent (and its source module) and misrouted them to the API handler.
Now matches on the exact pathname.
The mcp-oauth-consent e2e asserts the approval screen appears with the client
name, Allow grants a code, and gates the connection. Green on selfhost (vite)
and selfhost-docker (prod image); unit + lint + typecheck clean.
* e2e: real OpenCode binary through self-host MCP OAuth consent, spliced
Drives the actual opencode binary through the full MCP OAuth handshake against
self-host (its own DCR/PKCE/scopes/token store), with the browser hop driven by
a real Playwright session that lands on the forced /mcp-consent approval screen
and clicks Allow — the consent redirect goes to OpenCode's own localhost
callback, so OpenCode receives the code and finishes the grant for real
(asserts a stored access token + "connected").
The agent PTY (terminal.cast) and the browser (session.mp4) run concurrently
via Effect.all and mark focus terminal→browser→terminal, so scripts/film.ts
cuts them into one film.mp4: the agent asks → we approve in the browser → the
agent is connected. Green on selfhost (vite) and selfhost-docker (prod image);
skips where the opencode binary isn't installed.
This is the real-client + spliced-recording coverage that previously lived only
on the unmerged test/selfhost-e2e-org-setup branch (record-flow-oauth.ts).
* e2e: framework-managed human dwells, gated by E2E_FILM
A scenario author shouldn't hand-code waitForTimeout to make a recording
readable — pacing is the recording's concern, not the scenario's. A dwell is a
property of a focus transition: tabbing between tools, a developer lingers a
beat to take in where they landed. So the framework owns it:
- timeline.ts gains beat()/enterFocus() + isFilming(). beat() is a no-op unless
the run is being filmed (E2E_FILM=1, also implied by the desk's E2E_DESK).
- The browser step() enters focus (lingering on the outgoing window) and beats
at the end so each named step's result holds on screen.
- The Cli surface holds the terminal's final frame at end of drive.
So fast/CI runs pay nothing, and a filmed run paces itself into a watchable
'moving between apps' splice. The OpenCode consent scenario drops all its
hand-rolled dwells and just names two browser steps + enterFocus at the
terminal transitions — verified: 2.9s film unfilmed, 7.2s paced under
E2E_FILM=1, green both ways.
* docs(e2e): add the OpenCode self-host MCP OAuth consent recording
The paced film (E2E_FILM=1) of selfhost/mcp-opencode-consent — the real
opencode binary asking to connect, the browser approval screen, and connected
— embedded in the PR so reviewers see the flow, not just read about it.
* Self-host: drive mcp-oauth end-to-end test through the consent screen
The forced MCP approval screen makes /authorize redirect to /mcp-consent with a
consent_code instead of straight to the client callback. Update the opaque-bearer
end-to-end test to approve on the consent screen (POST /api/auth/oauth2/consent)
and follow the returned redirectURI to the code, matching what a real client's
browser does.1 parent b998f8d commit 64b3544
20 files changed
Lines changed: 879 additions & 19 deletions
File tree
- .changeset
- apps/host-selfhost
- src
- auth
- mcp
- web
- routes
- e2e
- recordings
- selfhost
- src
- surfaces
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
96 | | - | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
97 | 108 | | |
98 | 109 | | |
99 | 110 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
37 | 38 | | |
38 | 39 | | |
39 | 40 | | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
40 | 72 | | |
41 | 73 | | |
42 | | - | |
| 74 | + | |
43 | 75 | | |
44 | 76 | | |
45 | 77 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
99 | | - | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
100 | 104 | | |
101 | 105 | | |
102 | 106 | | |
| |||
114 | 118 | | |
115 | 119 | | |
116 | 120 | | |
117 | | - | |
118 | | - | |
119 | | - | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
120 | 140 | | |
121 | 141 | | |
122 | 142 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
19 | 24 | | |
20 | | - | |
| 25 | + | |
21 | 26 | | |
22 | 27 | | |
23 | 28 | | |
| 29 | + | |
24 | 30 | | |
25 | 31 | | |
26 | 32 | | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
27 | 51 | | |
28 | 52 | | |
29 | 53 | | |
| |||
35 | 59 | | |
36 | 60 | | |
37 | 61 | | |
38 | | - | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
39 | 65 | | |
40 | 66 | | |
41 | 67 | | |
| |||
0 commit comments