mcp radio: add dj_station (new tool, new ui://gandora/station URI) to escape claude.ai's cached v1 card

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mBuCaQSgWwqhtzsmTBQuM
This commit is contained in:
teahubxyzandClaude Opus 5.5 committed 2026-10-22 19:51:51 +00:00
1 parent 1599709315
commit 2b1993ba9f
1 file changed
+7 -3
+7 -3
View File
@@ -9,7 +9,7 @@ const MEDIA = "https://dev.gandora.lol";
const ORIGIN = "https://dev.gandora.lol";
const TRACKS = { sky: "/asmr/audio/kokoro-sky.mp3", rain: "/asmr/audio/bed-rain.mp3" };
const PROBE_URI = "ui://gandora/probe";
const RADIO_URI = "ui://gandora/radio-v3";
const RADIO_URI = "ui://gandora/station";
const UI_MIME = "text/html;profile=mcp-app";
const UI_META = { ui: { csp: { resourceDomains: [MEDIA], connectDomains: [MEDIA] }, prefersBorder: true } };
const APP_ONLY = { ui: { visibility: ["app"] } };
@@ -36,6 +36,10 @@ const TOOLS = [
description: "Shows the gandora audio probe player in chat (an MCP App).",
inputSchema: { type: "object", properties: {} },
_meta: { ui: { resourceUri: PROBE_URI }, "ui/resourceUri": PROBE_URI } },
{ name: "dj_station", title: "Open a gandora station",
description: "Preferred over dj_play. You are the gandora DJ: write the opening track sheet plus a station theme. A self-running station card appears in chat: it renders and plays your track, then keeps the station going by itself (a small DJ model writes each following sheet, steered by the listener's Like / Not this / Next), with no messages to the chat. Its play log reaches you as model context. Call again only for a new station or a change of direction.",
inputSchema: SHEET,
_meta: { ui: { resourceUri: RADIO_URI }, "ui/resourceUri": RADIO_URI } },
{ name: "dj_play", title: "Play a track on gandora",
description: "You are the gandora DJ. Write the opening track sheet (and a station theme) and put it on air: a player card appears in chat, renders the song (ACE-Step, ~15-60 s), speaks your dj_line, then plays. The card then keeps the station going on its own (a small DJ model writes the following sheets in your style, steered by the listener's Like / Not this / Next), so you do not need to call dj_play per track. Its play log and reactions reach you as model context. Call dj_play again only when the listener asks you for a change of direction or a new station.",
inputSchema: SHEET,
@@ -78,7 +82,7 @@ async function callTool(name, a, env) {
const r = await fetch(MEDIA + path); if (!r.ok) return fail("fetch " + r.status);
return { content: [{ type: "text", text: "clip" }], structuredContent: { id: a.id || "sky", mimeType: "audio/mpeg", data: b64(await r.arrayBuffer()) } };
}
if (name === "dj_play") {
if (name === "dj_play" || name === "dj_station") {
const sheet = { ...a, seconds: clamp(a.seconds, 30, 120, 60), seed: hash32(a.title + "|" + a.tags) };
return { content: [{ type: "text", text: `On air: "${a.title}" (${sheet.seconds}s). The card is rendering it now; wait for a [gandora] message before playing the next one.` }],
structuredContent: sheet };
@@ -143,7 +147,7 @@ async function handle(msg, env) {
case "resources/templates/list": return ok({ resourceTemplates: [] });
case "resources/read":
if (params && params.uri === PROBE_URI) return ok({ contents: [{ uri: PROBE_URI, mimeType: UI_MIME, text: PROBE_HTML, _meta: UI_META }] });
if (params && (params.uri === RADIO_URI || params.uri === "ui://gandora/radio")) return ok({ contents: [{ uri: RADIO_URI, mimeType: UI_MIME, text: RADIO_HTML, _meta: UI_META }] });
if (params && (params.uri === RADIO_URI || params.uri === "ui://gandora/radio" || params.uri === "ui://gandora/radio-v3")) return ok({ contents: [{ uri: RADIO_URI, mimeType: UI_MIME, text: RADIO_HTML, _meta: UI_META }] });
return err(-32002, "resource not found");
default:
if (id === undefined) return null;