tools/mcp-e2e
The hosted MCP (worker/src/hosted-mcp.ts, /ai/ on the site) end to end,
as a listener's own AI would use it. e2e.mjs is a real MCP client (the
TypeScript SDK's) driving a real browser (headless Chromium, with a virtual
passkey authenticator):
- The client connects, is refused, and is sent to authorize; the browser makes an account with a passkey on the authorize page, allows the app, and returns a code to the client's loopback redirect, as Claude Code does; the client swaps it for a token.
- The same browser opens the site signed in (
?session=e2ea1), and the header shows the tab is reachable. - The client lists the tools, reads the site's songs, plays one into the tab, and checks it plays in the sandbox and not the page, that the scheduler advances and samples load, that Jev answers, and that Jev's calls go out with the listener's session (charged to their budget).
- It publishes a song, which Jev screens, and finds it in
my_content. - A second listener connects their own app and sees only their own tab; playing into the first's tab by its id is refused.
- Disconnecting the first app in the account panel's API ends its token.
It runs against a dev server that is already up: the site with its Worker
beside it, the Worker with a Jev key (op-env-run). From a worktree, on
ports of its own:
op-env-run -- node tools/dev/worker.mjs --env dev --port 4392 --inspector-port 9392 \
--assets worker/.wrangler/dev-assets --test-scheduled \
--var SONGS_URL:http://localhost:4391/jev/songs.json
(cd website && JEV_WORKER_PORT=4392 pnpm exec astro dev --port 4391)
then, in the devshell, with Playwright and its browsers as the measure
app has them (the devshell does not set them: nix build .#measure --print-out-paths and copy the three export PLAYWRIGHT_… lines of its
bin/jevstrudel-measure), and the site's doc.json built once
(pnpm run jsdoc-json at the root; the dev server's reference reads it):
cd tools/mcp-e2e && npm install && SITE=http://localhost:4391 node e2e.mjs
tools.mjs is the tools both MCPs share (worker/README.md, The tools
both MCPs share), on the same servers, with the real Jev: a listener's AI
changes the tab's settings (opening the sounds tab), the browser imports a
sound folder there, then list_sounds per category (the import under user
and imported) and in pages, ask_jev_sound among the drum machines (in
banks) and the synths, charged to the listener, api_reference,
play_song for a site song (in the page) and a listener's (in the sandbox),
get_logs from the console tab, react, vote (in the summary),
ask_jev_song playing its pick, and comment; then the dev hub's MCP on
the same tab (WORKER, the Worker's own address, for /mcp):
SITE=http://localhost:4391 WORKER=http://localhost:4392 node tools.mjs
person.mjs is both scripts' listener: a passkey account made on the
authorize page, an MCP client with its token, and the signed-in tab.
proxy.mjs is the stdio proxy's prod mode (tools/mcp/) end to end: the
SDK's client starts tools/mcp/strudel-mcp.mjs over stdio, as Claude Code
does, with its prod endpoint pointed at a local production Worker. It
switches with use_environment while the headless browser signs in on the
authorize page the proxy logs, checks tools/list_changed and prod's tool
list, calls get_status and my_content through the proxy, disconnects
the app and signs in again from the error that follows, switches back to
dev, and checks the grant is revoked when the proxy exits. The Worker needs
no Jev key or built site for this; from a worktree root:
node tools/dev/worker.mjs --env "" --port 4397 --inspector-port 9397 \
--assets <an empty folder> --persist-to worker/.wrangler/standin-prod
after migrateLocal({ env: '', persistTo: 'worker/.wrangler/standin-prod' })
(tools/dev/migrate.mjs), then npm install in tools/mcp and here, and
SITE=http://localhost:4397 node proxy.mjs
e2e.mjs makes two accounts and a public listener song in that Worker's local
database each run (tools.mjs one account, a song, a comment and a vote;
proxy.mjs one account). Jev's answers to a request it has answered in the last
hour come from the relay's cache and are not charged, so a second run in
the hour shows its calls as cache hits.