The tools both MCPs share (worker/src/shared-tools.json) end to end, on a dev server of its own (README.md beside it): a listener's own AI through the hosted MCP, with the real Jev, and the dev hub's MCP on the same tab.
Hosted: list_sounds per category (a sound folder imported through the sounds tab shows under user and imported), ask_jev_sound (charged to the listener's budget), api_reference, a settings round trip (and a refused prebake), play_song for a site song (in the page) and a listener's song (in the sandbox), get_logs from the console tab, react, vote (counted in the summary), ask_jev_song, comment. Dev: the same tools listed and answered by the dev hub. Exits nonzero on any failed check.
15const WORKER = process.env.WORKER ?? 'http://localhost:4392'; 16const ROOT = new URL('../../', import.meta.url); 17 18try { 19 const link = await person('Tools', 'e2et1', 33411); 20 const { page, tool, budget } = link; 21 const names = (await link.client.listTools()).tools.map((t) => t.name); 22 const shared = ['list_sounds', 'ask_jev_sound', 'api_reference', 'get_settings', 'set_settings', 'play_song', 'ask_jev_song', 'react', 'vote']; 23 check(shared.every((n) => names.includes(n)) && names.includes('comment'), `hosted lists the shared tools and comment (${names.length} tools)`);
── settings: open the sounds tab by changing activeFooter, round trip ──
26 const before = JSON.parse((await tool('get_settings')).text); 27 check(before.themes.includes('dracula') && typeof before.settings.fontSize === 'number' && !('prebakeScript' in before.settings), `get_settings: ${JSON.stringify(before.settings).slice(0, 120)}…`); 28 const set = await tool('set_settings', { changes: { theme: 'dracula', fontSize: 22, activeFooter: 'sounds' } }); 29 check(!set.isError && /changed theme, fontSize, activeFooter/.test(set.text), `set_settings: ${set.text.slice(0, 100)}`); 30 const after = JSON.parse((await tool('get_settings')).text).settings; 31 check(after.theme === 'dracula' && after.fontSize === 22 && after.activeFooter === 'sounds', 'get_settings reads the changes back'); 32 const stored = await page.evaluate(() => Object.fromEntries(Object.keys(localStorage).filter((k) => k.startsWith('strudel-settings')).map((k) => [k, localStorage.getItem(k)]))); 33 check(Object.entries(stored).some(([k, v]) => k.endsWith('theme') && v === 'dracula'), "the page's settings store has them"); 34 const prebake = await tool('set_settings', { changes: { prebakeScript: 'fetch("/jev/auth/me")' } }); 35 check(prebake.isError && /not a setting an MCP may change/.test(prebake.text), `prebakeScript refused: ${prebake.text}`);
── import a sound folder through the sounds tab, as a listener would ──
38 const folder = new URL('../../worker/.wrangler/e2e-import/e2ewhoosh/', import.meta.url).pathname; 39 mkdirSync(folder, { recursive: true }); 40 const jeff = readdirSync(new URL('samples/jeff/', ROOT)).find((f) => /\.(mp3|wav)$/.test(f)); 41 copyFileSync(new URL(`samples/jeff/${jeff}`, ROOT), `${folder}${jeff}`); 42 await page.click('#importSounds'); 43 await page.setInputFiles('#audio_file', folder.replace(/\/$/, '')); 44 await page.waitForFunction(() => document.body.textContent.includes('e2ewhoosh'), null, { timeout: 15000 }).catch(() => {});
── list_sounds, per category ──
47 for (const category of ['samples', 'drum-machines', 'synths', 'wavetables', 'user', 'imported']) { 48 const r = await tool('list_sounds', { category, limit: 3 }); 49 const total = Number(/^(\d+) /.exec(r.text)?.[1] ?? 0); 50 check(!r.isError && total > 0, `list_sounds ${category}: ${total} sounds; ${r.text.split('\n').slice(1, 3).join(' | ')}`); 51 if (category === 'imported') check(r.text.includes('e2ewhoosh (imported') && /sandbox your code plays in cannot/.test(r.text), 'the imported folder is listed, with where it plays'); 52 } 53 const paged = await tool('list_sounds', { category: 'drum-machines', filter: '808', limit: 2 }); 54 const cursor = /cursor "(\d+)"/.exec(paged.text)?.[1]; 55 const next = await tool('list_sounds', { category: 'drum-machines', filter: '808', limit: 2, cursor }); 56 check(cursor === '2' && next.text.includes('; 3 to 4') && /\.bank\("\w*808\w*"\)/.test(paged.text), `pages: ${paged.text.split('\n')[0]} → ${next.text.split('\n')[0]}`);
── ask_jev_sound, the real Jev, charged to the listener ──
59 const spent0 = await budget(); 60 const kick = await tool('ask_jev_sound', { description: 'a deep, booming 808 kick drum', category: 'drum-machines' }); 61 log('ask_jev_sound (drum-machines):\n' + kick.text); 62 check(!kick.isError && /^Jev picks \S+/.test(kick.text) && /banks/.test(kick.text) && /finalists/.test(kick.text), 'ask_jev_sound banks the drum machines and says the odds are the finalists\''); 63 const keys = await tool('ask_jev_sound', { description: 'a warm electric piano for chords', category: 'synths' }); 64 log('ask_jev_sound (synths):\n' + keys.text); 65 check(!keys.isError && /^Jev picks \S+.*note\("c3 e3 g3"\)/.test(keys.text), 'ask_jev_sound among the synths'); 66 const spent1 = await budget(); 67 check(spent1 > spent0 || /no Jev call/.test(kick.text), `charged to the listener's budget (${spent0} → ${spent1}; a cached answer is free)`);
── api_reference ──
70 const search = await tool('api_reference', { query: 'low-pass filter' }); 71 check(!search.isError && /\blpf\b/.test(search.text), `api_reference query: ${search.text.split('\n').slice(0, 3).join(' | ')}`); 72 const lpf = await tool('api_reference', { name: 'lpf' }); 73 check(/^lpf\(frequency\)/.test(lpf.text) && lpf.text.includes('examples:'), `api_reference name: ${lpf.text.split('\n')[0]}`);
── play_song: a site song in the page ──
76 const songs = JSON.parse((await tool('list_songs')).text); 77 const played = await tool('play_song', { id: 'jev/dial-up' }); 78 check(!played.isError && /in the page/.test(played.text), `play_song site: ${played.text}`); 79 await page.waitForTimeout(4000); 80 const inPage = await page.evaluate(() => ({ 81 started: Boolean(window.strudelMirror?.repl?.scheduler?.started), 82 sandbox: document.querySelectorAll('iframe[sandbox]').length, 83 })); 84 check(inPage.started && inPage.sandbox === 0, `it plays in the page, no sandbox (${JSON.stringify(inPage)})`); 85 const consoleTab = await tool('get_logs', { source: 'console' }); 86 check(!consoleTab.isError && consoleTab.text !== 'nothing logged', `get_logs console: ${consoleTab.text.split('\n').slice(-3).join(' | ')}`);
── react while it plays ──
89 let reacted = { isError: true, text: '' }; 90 for (let i = 0; i < 20 && reacted.isError; i++) { 91 reacted = await tool('react', { kind: 'fire' }); 92 if (reacted.isError) await page.waitForTimeout(1000); 93 } 94 check(!reacted.isError && /Jev hears it/.test(reacted.text), `react: ${reacted.text}`);
── play_song: a listener's song in the sandbox ──
97 const mine = await tool('publish_song', { title: 'E2E Tools Beat', description: 'a beat to play by id', code: 'setcps(1)\ns("bd hh sd hh")' }); 98 const listenerId = JSON.parse(mine.text).id; 99 const theirs = await tool('play_song', { id: listenerId }); 100 check(!theirs.isError && /in its sandbox/.test(theirs.text), `play_song listener: ${theirs.text}`); 101 const inSandbox = await page.evaluate(() => ({ 102 started: Boolean(window.strudelMirror?.repl?.scheduler?.started), 103 frames: [...document.querySelectorAll('iframe[sandbox]')].map((f) => f.getAttribute('sandbox')), 104 })); 105 check(!inSandbox.started && inSandbox.frames.length === 1 && inSandbox.frames[0] === 'allow-scripts', `it plays in the sandbox, not the page (${JSON.stringify(inSandbox)})`); 106 const status = await tool('get_status'); 107 check(status.text.includes("a listener's song (sandboxed)"), `get_status: ${status.text.split('\n')[1]}`); 108 await page.waitForTimeout(2000); 109 const sandboxLogs = await tool('get_logs'); 110 check(/load sound|\[sampler\]/.test(sandboxLogs.text), `get_logs has the sandbox's: ${sandboxLogs.text.split('\n')[0]}`);
── vote ──
113 const scored = songs.site.filter((s) => typeof s.art === 'number'); 114 const [a, b] = scored.sort((x, y) => y.art - x.art); 115 const voted = await tool('vote', { a: a.id, b: b.id, pick: a.id }); 116 check(!voted.isError && /counted/.test(voted.text) && /agree with Jev/.test(voted.text), `vote: ${voted.text}`); 117 const summary = await fetch(`${SITE}/jev/votes/summary`).then((r) => r.json()); 118 check(summary.votes.some((v) => v.a === a.id && v.b === b.id && v.pick === a.id && v.n >= 1), 'the vote is in the summary'); 119 const badVote = await tool('vote', { a: a.id, b: 'jev/not-a-song', pick: a.id }); 120 check(badVote.isError && /not a song/.test(badVote.text), `a vote on no song is refused: ${badVote.text}`);
── ask_jev_song, playing Jev's pick ──
123 const mood = await tool('ask_jev_song', { mood: 'sleepy on a rainy sunday, want something gentle', play: true }); 124 check(!mood.isError && /^Jev picks .+ \((\w+\/[\w-]+)\).*playing it in the page/.test(mood.text), `ask_jev_song: ${mood.text}`); 125 const pickedId = /\((\w+\/[\w-]+)\)/.exec(mood.text)?.[1]; 126 await page.waitForTimeout(1500); 127 check(await page.evaluate(() => Boolean(window.strudelMirror?.repl?.scheduler?.started)), `${pickedId} plays in the page`);
── comment ──
── the dev hub's MCP, on the same tab ──
137 let id = 0; 138 const dev = async (method, params = {}) => { 139 const res = await fetch(`${WORKER}/mcp`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: ++id, method, params }) }); 140 return (await res.json()).result; 141 }; 142 const devNames = (await dev('tools/list')).tools.map((t) => t.name); 143 check(shared.every((n) => devNames.includes(n)) && !devNames.includes('comment'), `dev lists the shared tools: ${devNames.join(', ')}`); 144 const devCall = async (name, args) => (await dev('tools/call', { name, arguments: { session_id: 'e2et1', ...args } })).content[0].text; 145 const devSynths = await devCall('list_sounds', { category: 'synths', limit: 2 }); 146 check(/^\d+ synths sounds/.test(devSynths), `dev list_sounds: ${devSynths.split('\n')[0]}`); 147 const devSettings = JSON.parse(await devCall('get_settings', {})); 148 check(devSettings.settings.theme === before.settings.theme, `dev get_settings: theme ${devSettings.settings.theme}`); 149 const devRef = await devCall('api_reference', { name: 'fast' }); 150 check(/^fast\(/.test(devRef), `dev api_reference: ${devRef.split('\n')[0]}`); 151 const devPlay = await devCall('play_song', { id: 'jev/dial-up' }); 152 check(/in the page/.test(devPlay), `dev play_song: ${devPlay}`); 153 await devCall('stop_play', {}); 154} catch (e) { 155 fail(e.stack ?? e); 156} finally { 157 await browser.close(); 158}