What an MCP may ask a tab besides playing code: the tools both MCPs share
(worker/src/shared-tools.json), answered from what the tab's panel shows.
The dev hub's tab (repl/useWebSocketMCP.jsx) and the hosted MCP's
(myTabs.mjs) both hand these commands here; tabCommands.mjs supplies the
page's real modules as deps, tests their own.
list-sounds { category?, filter?, offset, limit } the sounds tab's sounds ask-sound { description, category?, filter? } Jev picks one (soundPick.mjs) get-settings the settings tab's, as allowed set-settings { changes } (worker/src/mcp-settings.mjs) get-console the console tab's lines play-song { id } a song, as its card plays it ask-song { mood, play } the mood picker (picker.mjs) react { kind } the booth's 🔥/😴, now
Nothing here evaluates code the MCP sent. A site song is the page's own, looked up by id in this page's own build, and plays in the page as a click on its card does; a listener's song is fetched from the site and plays in the sandbox, as its card does (sandbox.mjs). Jev's calls go through the relay from this page (ask.mjs), with its session, charged to the signed-in listener's budget as the mood picker's are.
22import { whyNotChanges, settingsView } from '../../../worker/src/mcp-settings.mjs'; 23import { playing } from './boothStore.mjs'; 24import { htmlText } from './reference.mjs'; 25import { CATEGORIES, soundEntry, soundList } from './sounds.mjs'; 26import { DESCRIPTION_MAX, pickSound } from './soundPick.mjs';
28export const TAB_TOOLS = new Set(['list-sounds', 'ask-sound', 'get-settings', 'set-settings', 'get-console', 'play-song', 'ask-song', 'react']);
as the hub bounds them (worker/src/tab-hub-core.ts)
The foreign code this module last played (a listener's song), so the hosted MCP's get_logs keeps what its sandbox logs (myTabs.mjs).
41const category = (c) => { 42 if (c === undefined || c === null || c === '' || c === 'all') return undefined; 43 if (!CATEGORIES.includes(c)) throw new Error(`category is one of ${CATEGORIES.join(', ')}`); 44 return c; 45}; 46const filterOf = (f) => (typeof f === 'string' ? f.trim().slice(0, 64) : ''); 47 48async function playSiteSong(editor, song, deps) { 49 if (!deps.audioRunning()) return { error: CLICK }; 50 await deps.playSong(editor, song); 51 const err = editor.repl?.state?.evalError; 52 if (err) return { error: err.message ?? String(err) }; 53 return { song: { id: song.id, title: song.title, where: 'page' } }; 54}
One command, answered; undefined when it is not one of these.
57export async function runTabTool(editor, command, deps) { 58 if (!TAB_TOOLS.has(command?.type)) return undefined; 59 if (!editor) return { error: 'the editor is not ready' }; 60 switch (command.type) { 61 case 'list-sounds': { 62 const all = soundList(deps.sounds(), { category: category(command.category), filter: filterOf(command.filter) }); 63 const offset = Math.max(0, Math.floor(Number(command.offset) || 0)); 64 const limit = Math.min(MAX_SOUNDS, Math.max(1, Math.floor(Number(command.limit) || 100))); 65 const items = all.slice(offset, offset + limit).map(([name, sound]) => soundEntry(name, sound?.data)); 66 return { sounds: { total: all.length, items } }; 67 } 68 case 'ask-sound': { 69 const description = typeof command.description === 'string' ? command.description.trim().slice(0, DESCRIPTION_MAX) : ''; 70 if (!description) return { error: 'description is what sound you want' }; 71 const sounds = soundList(deps.sounds(), { category: category(command.category), filter: filterOf(command.filter) }).map(([name, sound]) => 72 soundEntry(name, sound?.data), 73 ); 74 return { pick: await pickSound(sounds, description, { ask: deps.askJev }) }; 75 } 76 case 'get-settings': 77 return { settings: settingsView(deps.settings.get()), themes: deps.themes() }; 78 case 'set-settings': { 79 const themes = deps.themes(); 80 const why = whyNotChanges(command.changes, themes); 81 if (why) return { error: why }; 82 for (const [key, value] of Object.entries(command.changes)) deps.settings.setKey(key, value); 83 return { settings: settingsView(deps.settings.get()), themes }; 84 } 85 case 'get-console': 86 return { 87 logs: deps.consoleLog().map((l) => ({ at: 0, message: htmlText(l.message), kind: String(l.type ?? ''), count: l.count ?? 1 })), 88 }; 89 case 'play-song': { 90 const id = command.id; 91 const listener = typeof id === 'string' ? LISTENER_SONG.exec(id) : null; 92 if (listener) { 93 const song = await deps.loadListener(listener[1]); 94 await deps.showListener(editor, song); 95 played = deps.foreign(); 96 const result = await deps.playForeign(song.foreignCode); 97 if (result?.error) return { error: result.error }; 98 return { song: { id: song.id, title: song.title, where: 'sandbox' } }; 99 } 100 if (typeof id !== 'string' || !SITE_SONG.test(id)) return { error: 'id is "<theme>/<song>" or "listener:<id>", from list_songs' }; 101 const song = deps.readSongs().find((s) => s.id === id && s.code); 102 if (!song) return { error: `this tab has no song ${id}` }; 103 return playSiteSong(editor, song, deps); 104 } 105 case 'ask-song': { 106 const mood = typeof command.mood === 'string' ? command.mood.trim().slice(0, MOOD_MAX) : ''; 107 if (!mood) return { error: 'mood is how the listener feels' }; 108 if (command.play && !deps.audioRunning()) return { error: CLICK }; 109 const { song, p, confidence } = await deps.pickByMood(deps.readSongs(), mood); 110 const picked = { id: song.id, title: song.title, p: p ?? null, confidence: confidence ?? null }; 111 if (!command.play) return { song: picked }; 112 const result = await playSiteSong(editor, song, deps); 113 return result.error ? result : { song: { ...picked, where: 'page' } }; 114 } 115 case 'react': { 116 if (command.kind !== 'fire' && command.kind !== 'sleep') return { error: 'kind is fire or sleep' }; 117 const booth = deps.booth(); 118 const now = playing(booth, deps.cycle(editor)); 119 if (!now) return { error: 'no section of a song with Jev is playing in this tab' }; 120 const { section, counted } = deps.press(booth, now.segment, command.kind, editor); 121 return { reaction: { section: section ?? String(now.segment + 1), kind: command.kind, counted } }; 122 } 123 } 124}