jevstrudel.git / worker / src / hosted-tools.ts
1// The hosted MCP's scopes and tools (hosted-mcp.ts runs them), apart and
2// importing nothing but the tools it shares with the dev hub
3// (shared-tools.json, data), so the site's "Make songs with your AI" page
4// (website/src/pages/ai.astro) lists exactly what the server offers.
5//
6// Scopes. The tab tools are `play`: what a tab has (its sounds, its
7// settings, its console) and what it does (play code or a song, react,
8// ask Jev from the page, charged to your budget as the page's own Jev calls
9// are). Changing the editor's settings is `play` too, not a scope of its
10// own: the settings an MCP may touch are only how the editor looks and
11// types (mcp-settings.mjs refuses the rest), each undone in the settings
12// tab, and an app allowed to replace the editor's code and play sound in
13// the tab is trusted with more than its font. `publish` is anything under
14// your name that others read (songs, comments). Reading the site's
15// reference and songs, and an anonymous vote (anyone may vote signed out),
16// need only a valid token.
17import type { Tool } from './mcp-rpc';
18import shared from './shared-tools.json';
19
20export const SCOPES = {
21  play: 'use your open jevstrudel tabs: play code (in the sandbox) or songs, read what they logged, their sounds and settings, change their editor settings, react, and ask Jev there (charged to your daily Jev budget)',
22  publish: 'publish and revise listener songs and comment under your name, and see their status',
23} as const;
24export type Scope = keyof typeof SCOPES;
25const session = { type: 'string', description: 'Which of your tabs (get_status lists them). Optional when only one is open.' };
26const songFieldsSchema = {
27  title: { type: 'string', description: 'At most 80 characters, one line.' },
28  description: { type: 'string', description: 'At most 280 characters, one line. Shown on its card.' },
29  spec: { type: 'string', description: 'What the song is meant to be, in markdown (at most 32 KiB): the prompt, the idea, the form.' },
30  code: { type: 'string', description: 'The Strudel code, at most 64 KiB. It must make music and nothing else.' },
31};
32
33export const TOOLS: (Tool & { scope: Scope | null })[] = [
34  {
35    name: 'get_status',
36    scope: 'play',
37    description:
38      "List your open jevstrudel tabs (pages open on the site, signed in to your account) and what each is playing. Tabs appear here once they are open; a tab must have been clicked once before it can make sound.",
39    inputSchema: { type: 'object', properties: {} },
40  },
41  {
42    name: 'play_code',
43    scope: 'play',
44    description:
45      "Load Strudel code into one of your tabs' editor and play it from the top. It plays in the tab's sandbox, apart from the page and your account, as any code the site did not write does; Jev's calls from it (jev()) count against your daily Jev budget. Returns the evaluation error, if any.",
46    inputSchema: {
47      type: 'object',
48      properties: { code: { type: 'string', description: 'Strudel code, at most 64 KiB' }, session_id: session },
49      required: ['code'],
50    },
51  },
52  {
53    name: 'stop_play',
54    scope: 'play',
55    description: 'Stop playback in one of your tabs.',
56    inputSchema: { type: 'object', properties: { session_id: session } },
57  },
58  {
59    name: 'get_logs',
60    scope: 'play',
61    description:
62      "What your tab's sandbox has logged since the last play_code or play_song: Strudel's console (warnings, evaluation and scheduler errors, [jev] choices, sample loads), oldest first, with repeats counted. source \"console\" is the page's console tab instead (its last 40 lines): what a site song playing in the page logged.",
63    inputSchema: {
64      type: 'object',
65      properties: {
66        session_id: session,
67        filter: { type: 'string', description: 'Only lines containing this text, e.g. "warn" or "error".' },
68        source: { type: 'string', enum: ['sandbox', 'console'], description: 'Default sandbox.' },
69      },
70    },
71  },
72  {
73    name: 'get_currently_playing_code',
74    scope: 'play',
75    description: "Return the code in one of your tabs' editor.",
76    inputSchema: { type: 'object', properties: { session_id: session } },
77  },
78  {
79    name: 'list_songs',
80    scope: null,
81    description:
82      "List the site's own songs (id \"<theme>/<song>\") and listeners' public songs (id \"listener:<id>\"), with their titles and Jev's art score (0 to 1).",
83    inputSchema: { type: 'object', properties: {} },
84  },
85  {
86    name: 'get_song',
87    scope: null,
88    description: "One song whole: its spec (what it was made from) and its code. Takes an id from list_songs. Good to learn from before writing your own.",
89    inputSchema: { type: 'object', properties: { id: { type: 'string', description: '"<theme>/<song>" or "listener:<id>"' } }, required: ['id'] },
90  },
91  {
92    name: 'publish_song',
93    scope: 'publish',
94    description:
95      "Publish a new listener song under your name. Jev screens it first (spam, abuse, or code that does anything but make music is held); once fine it is public on the site and Jev scores it for art. Returns its id and status. Screening and scoring count against your daily Jev budget.",
96    inputSchema: { type: 'object', properties: songFieldsSchema, required: ['title', 'code'] },
97  },
98  {
99    name: 'revise_song',
100    scope: 'publish',
101    description: 'Publish the next revision of one of your own songs (whole: title, description, spec and code). It is screened again before it replaces the public one.',
102    inputSchema: {
103      type: 'object',
104      properties: { id: { type: 'string', description: 'The song, "listener:<id>" (or the bare id)' }, ...songFieldsSchema },
105      required: ['id', 'title', 'code'],
106    },
107  },
108  {
109    name: 'comment',
110    scope: 'publish',
111    description: 'Comment on a song under your name. Jev screens it first, as every comment; returns its status.',
112    inputSchema: {
113      type: 'object',
114      properties: {
115        song: { type: 'string', description: '"<theme>/<song>" or "listener:<id>"' },
116        body: { type: 'string', description: 'At most 2000 characters.' },
117      },
118      required: ['song', 'body'],
119    },
120  },
121  {
122    name: 'my_content',
123    scope: 'publish',
124    description:
125      'Everything you have published (song revisions, covers, comments, pitches), each public, held (Jev\'s verdict) or pending (why, and when Jev is asked again).',
126    inputSchema: { type: 'object', properties: {} },
127  },
128  ...(shared as (Tool & { scope: Scope | null })[]),
129];