jevstrudel.git / tools / mcp-e2e

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):

  1. 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.
  2. The same browser opens the site signed in (?session=e2ea1), and the header shows the tab is reachable.
  3. 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).
  4. It publishes a song, which Jev screens, and finds it in my_content.
  5. A second listener connects their own app and sees only their own tab; playing into the first's tab by its id is refused.
  6. 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.