The e2e scripts' shared half (README.md beside it): a listener with an account, an MCP client authorized as them, and their signed-in tab.
8export const SITE = process.env.SITE ?? 'http://localhost:4391'; 9export const MCP = new URL('/jev/mcp', SITE); 10const { chromium } = await import(process.env.PLAYWRIGHT_MODULE); 11export const browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required'] }); 12export const log = (...a) => console.log('[e2e]', ...a); 13export const fail = (why) => { 14 console.error('[e2e] FAIL:', why); 15 process.exitCode = 1; 16}; 17export const check = (ok, what) => (ok ? log('ok:', what) : fail(what)); 18 19class Provider { 20 constructor(name, port) { 21 this.redirect = `http://127.0.0.1:${port}/callback`; 22 this.name = name; 23 this.store = {}; 24 } 25 get redirectUrl() { 26 return this.redirect; 27 } 28 get clientMetadata() { 29 return { 30 client_name: this.name, 31 redirect_uris: [this.redirect], 32 grant_types: ['authorization_code', 'refresh_token'], 33 response_types: ['code'], 34 token_endpoint_auth_method: 'none', 35 }; 36 } 37 clientInformation() { 38 return this.store.client; 39 } 40 saveClientInformation(c) { 41 this.store.client = c; 42 } 43 tokens() { 44 return this.store.tokens; 45 } 46 saveTokens(t) { 47 this.store.tokens = t; 48 } 49 redirectToAuthorization(url) { 50 this.authUrl = url; 51 } 52 saveCodeVerifier(v) { 53 this.store.verifier = v; 54 } 55 codeVerifier() { 56 return this.store.verifier; 57 } 58}
One listener: a browser with a passkey, an account made on the authorize
page, an MCP client with its token, and the site open signed in as session.
62export async function person(name, session, port) { 63 const context = await browser.newContext(); 64 const page = await context.newPage(); 65 page.on('pageerror', (e) => log(`${name} page error:`, e.message)); 66 page.on('websocket', (w) => { 67 if (!w.url().includes('/jev/me/tabs')) return; 68 log(`${name} tab socket open`); 69 w.on('close', () => log(`${name} tab socket closed`)); 70 w.on('socketerror', (e) => log(`${name} tab socket error`, e)); 71 }); 72 const cdp = await context.newCDPSession(page); 73 await cdp.send('WebAuthn.enable'); 74 await cdp.send('WebAuthn.addVirtualAuthenticator', { 75 options: { protocol: 'ctap2', transport: 'internal', hasResidentKey: true, hasUserVerification: true, isUserVerified: true, automaticPresenceSimulation: true }, 76 });
- the MCP client tries, is told to authorize
79 const provider = new Provider(`E2E AI for ${name}`, port); 80 let transport = new StreamableHTTPClientTransport(MCP, { authProvider: provider }); 81 let client = new Client({ name: 'e2e', version: '1.0.0' }); 82 try { 83 await client.connect(transport); 84 fail('connected without authorizing'); 85 } catch (e) { 86 check(e instanceof UnauthorizedError && provider.authUrl, `${name}: the client is sent to authorize (${provider.authUrl?.pathname})`); 87 }
- the browser: sign up on the authorize page with a passkey, then consent the client's loopback redirect, as Claude Code listens on one
91 let callback = null; 92 const server = createServer((req, res) => { 93 callback = new URL(req.url, `http://127.0.0.1:${port}`); 94 res.end('done'); 95 }); 96 await new Promise((r) => server.listen(port, '127.0.0.1', r)); 97 await page.goto(provider.authUrl.href); 98 check(await page.getByText('sign in with a passkey').isVisible(), `${name}: the authorize page asks to sign in`); 99 await page.fill('#name', name); 100 await Promise.all([page.waitForEvent('load'), page.click('#create button')]); 101 await page.waitForSelector('button[value=approve]', { timeout: 15000 }); 102 const consent = await page.textContent('main'); 103 check(consent.includes(`E2E AI for ${name}`) && consent.includes(`Signed in as ${name}`), `${name}: consent names the app and the account`); 104 page.on('console', (m) => log(`${name} console:`, m.text())); 105 page.on('response', (r) => r.url().includes('/jev/oauth/') && log(`${name} response:`, r.status(), r.request().method(), r.headers()['location'] ?? '')); 106 await page.click('button[value=approve]'); 107 try { 108 await page.waitForURL(`http://127.0.0.1:${port}/**`, { timeout: 15000 }); 109 } catch (e) { 110 log('stuck at', page.url(), (await page.textContent('body')).slice(0, 300)); 111 throw e; 112 } 113 check(callback?.searchParams.get('code'), `${name}: redirected back with a code`); 114 check(callback?.searchParams.get('iss') === SITE, `${name}: the redirect carries iss`); 115 server.close(); 116 await transport.finishAuth(callback.searchParams.get('code'));
- connected
119 transport = new StreamableHTTPClientTransport(MCP, { authProvider: provider }); 120 client = new Client({ name: 'e2e', version: '1.0.0' }); 121 await client.connect(transport); 122 log(`${name}: connected; server ${JSON.stringify(client.getServerVersion())}, protocol ${transport.protocolVersion}`);
- the tab, signed in by the same browser
125 await page.goto(`${SITE}/?session=${session}`); 126 try { 127 // the jev panel's mcp tab says when the AI is connected to this tab 128 await page.click('[data-testid=jev-panel] button:has-text("mcp")', { timeout: 60000 }); 129 await page.waitForFunction( 130 (s) => document.querySelector('[data-testid=jev-my-tab]')?.textContent.includes(s), 131 session, 132 { timeout: 60000 }, 133 ); 134 } catch (e) { 135 const probe = await page.evaluate(async () => { 136 const me = await fetch('/jev/auth/me').then((r) => r.json()); 137 const ws = await new Promise((resolve) => { 138 const s = new WebSocket(`ws://${location.host}/jev/me/tabs?session_id=probe1`); 139 s.onopen = () => resolve('open'); 140 s.onerror = () => resolve('error'); 141 s.onclose = (e) => resolve(`closed ${e.code}`); 142 }); 143 return { me, ws, account: document.querySelector('[data-testid=jev-account]')?.textContent }; 144 }); 145 log('tab probe:', JSON.stringify(probe)); 146 throw e; 147 } 148 await page.mouse.click(400, 400); 149 check((await page.textContent('[data-testid=jev-my-tab]')).includes(session), `${name}: the mcp tab shows 🤖 ${session}`); 150 const tool = async (name, args = {}) => { 151 const r = await client.callTool({ name, arguments: args }); 152 return { text: r.content?.[0]?.text ?? '', isError: !!r.isError }; 153 }; 154 const budget = () => page.evaluate(() => fetch('/jev/auth/me').then((r) => r.json()).then((b) => b.budget?.used ?? null)); 155 return { page, context, client, tool, budget, provider }; 156}