vibo-mcp 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +50 -1
- package/dist/auth.js +7 -3
- package/dist/bundle.js +675 -81
- package/dist/client.js +13 -1
- package/dist/gql.js +50 -0
- package/dist/index.js +2 -0
- package/dist/reorder.js +27 -0
- package/dist/tools/lookups.js +59 -0
- package/dist/tools/section-manage.js +288 -0
- package/dist/tools/session.js +1 -1
- package/dist/tools/shared.js +8 -3
- package/dist/tools/song-management.js +70 -10
- package/dist/tools/songs.js +44 -3
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
- package/skills/vibo-mcp/SKILL.md +11 -4
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { McpToolError, minifiedResult, confirmTokenParam, toolAnnotations } from '@chrischall/mcp-utils';
|
|
3
3
|
import { REMOVE_SECTION_SONGS, UPDATE_SECTION_SONGS, MOVE_SECTION_SONGS, REORDER_SONGS } from '../gql.js';
|
|
4
|
+
import { planMoves } from '../reorder.js';
|
|
4
5
|
import { confirmWrite, CONFIRM_NOTE } from './shared.js';
|
|
6
|
+
import { fetchSections, fetchEventPermissions, fetchSectionSongs, findSection, assertSongsInSection, isHost, } from './lookups.js';
|
|
7
|
+
/**
|
|
8
|
+
* Vibo's song-comment limit, measured live: 90 is stored, 91 is refused with
|
|
9
|
+
* "Comment should be less then 90 characters". Counted in UTF-16 units (an
|
|
10
|
+
* emoji is 2), which is what String#length gives.
|
|
11
|
+
*/
|
|
12
|
+
export const SONG_COMMENT_MAX = 90;
|
|
5
13
|
export function registerSongManagementTools(server, client) {
|
|
6
14
|
server.registerTool('vibo_remove_song_from_section', {
|
|
7
|
-
description: 'Remove one or more songs from a section. ' +
|
|
15
|
+
description: 'Remove one or more songs from a section. Every id is checked against the section first; if any is not ' +
|
|
16
|
+
'there, nothing is sent and the missing ids are listed. ' + CONFIRM_NOTE,
|
|
8
17
|
annotations: toolAnnotations({ title: 'Remove songs from Vibo section', readOnly: false, destructive: true }),
|
|
9
18
|
inputSchema: z.object({
|
|
10
19
|
eventId: z.string().describe('Event id.'),
|
|
@@ -16,6 +25,10 @@ export function registerSongManagementTools(server, client) {
|
|
|
16
25
|
confirmToken: confirmTokenParam,
|
|
17
26
|
}),
|
|
18
27
|
}, async ({ eventId, sectionId, songIds, confirmToken }, ctx) => {
|
|
28
|
+
// Vibo answers success:true for ids that aren't in the section and removes
|
|
29
|
+
// nothing, so check them ourselves before asking the user to confirm.
|
|
30
|
+
const songs = await fetchSectionSongs(client, eventId, sectionId);
|
|
31
|
+
assertSongsInSection(songIds, songs);
|
|
19
32
|
const vars = { eventId, sectionId, songIds };
|
|
20
33
|
const gate = await confirmWrite(ctx, {
|
|
21
34
|
tool: 'vibo_remove_song_from_section',
|
|
@@ -24,6 +37,12 @@ export function registerSongManagementTools(server, client) {
|
|
|
24
37
|
confirmToken,
|
|
25
38
|
target: sectionId,
|
|
26
39
|
willSend: vars,
|
|
40
|
+
context: {
|
|
41
|
+
songs: songIds.map((id) => {
|
|
42
|
+
const s = songs.find((x) => x._id === id);
|
|
43
|
+
return `${s.title ?? '?'} — ${s.artist ?? '?'}`;
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
27
46
|
});
|
|
28
47
|
if (gate)
|
|
29
48
|
return gate;
|
|
@@ -31,7 +50,9 @@ export function registerSongManagementTools(server, client) {
|
|
|
31
50
|
return minifiedResult(data.removeSectionSongsV2);
|
|
32
51
|
});
|
|
33
52
|
server.registerTool('vibo_update_song', {
|
|
34
|
-
description: 'Update songs in a section: mark must-play, flag as do-not-play, and/or set a comment. Provide at least one field. ' +
|
|
53
|
+
description: 'Update songs in a section: mark must-play, flag as do-not-play, and/or set a comment. Provide at least one field. ' +
|
|
54
|
+
`A comment can be at most ${SONG_COMMENT_MAX} characters (Vibo's limit; emoji count double; checked before anything is sent). ` +
|
|
55
|
+
CONFIRM_NOTE,
|
|
35
56
|
annotations: toolAnnotations({ title: 'Update Vibo section songs', readOnly: false, destructive: false }),
|
|
36
57
|
inputSchema: z.object({
|
|
37
58
|
eventId: z.string().describe('Event id.'),
|
|
@@ -42,7 +63,7 @@ export function registerSongManagementTools(server, client) {
|
|
|
42
63
|
.describe('Song _ids from vibo_get_section_songs.'),
|
|
43
64
|
isMustPlay: z.boolean().optional(),
|
|
44
65
|
isFlagged: z.boolean().optional().describe('mark do-not-play / flagged'),
|
|
45
|
-
comment: z.string().optional(),
|
|
66
|
+
comment: z.string().optional().describe(`Comment for the DJ, at most ${SONG_COMMENT_MAX} characters.`),
|
|
46
67
|
confirmToken: confirmTokenParam,
|
|
47
68
|
}),
|
|
48
69
|
}, async ({ eventId, sectionId, songIds, isMustPlay, isFlagged, comment, confirmToken }, ctx) => {
|
|
@@ -58,6 +79,9 @@ export function registerSongManagementTools(server, client) {
|
|
|
58
79
|
hint: 'Pass isMustPlay, isFlagged, and/or comment to update the songs.',
|
|
59
80
|
});
|
|
60
81
|
}
|
|
82
|
+
if (comment !== undefined && comment.length > SONG_COMMENT_MAX) {
|
|
83
|
+
throw new McpToolError(`Comment is ${comment.length} characters; Vibo allows at most ${SONG_COMMENT_MAX}. Nothing was sent.`, { hint: `Shorten it to ${SONG_COMMENT_MAX} characters or fewer, or put the longer text in a section note.` });
|
|
84
|
+
}
|
|
61
85
|
const vars = { eventId, sectionId, songIds, payload };
|
|
62
86
|
const gate = await confirmWrite(ctx, {
|
|
63
87
|
tool: 'vibo_update_song',
|
|
@@ -101,7 +125,12 @@ export function registerSongManagementTools(server, client) {
|
|
|
101
125
|
return minifiedResult(data.moveSectionSongsV2);
|
|
102
126
|
});
|
|
103
127
|
server.registerTool('vibo_reorder_songs', {
|
|
104
|
-
description: 'Reorder songs within a section
|
|
128
|
+
description: 'Reorder songs within a section: move sourceSongIds (in the given order) to directly after targetSongId, ' +
|
|
129
|
+
'or to the top when it is omitted. Checks the ids against the section and, for a host, that the DJ allows ' +
|
|
130
|
+
"hosts to order this section's songs. That setting is OFF for every section a host creates and only the " +
|
|
131
|
+
'DJ can turn it on, so a host usually cannot reorder songs in sections they added. Sends one ' +
|
|
132
|
+
'reorderSongsBatch call per song that actually moves. ' +
|
|
133
|
+
CONFIRM_NOTE,
|
|
105
134
|
annotations: toolAnnotations({ title: 'Reorder Vibo section songs', readOnly: false, destructive: false }),
|
|
106
135
|
inputSchema: z.object({
|
|
107
136
|
eventId: z.string().describe('Event id.'),
|
|
@@ -109,26 +138,57 @@ export function registerSongManagementTools(server, client) {
|
|
|
109
138
|
sourceSongIds: z
|
|
110
139
|
.array(z.string())
|
|
111
140
|
.min(1)
|
|
112
|
-
.describe('Song _ids from vibo_get_section_songs.'),
|
|
141
|
+
.describe('Song _ids from vibo_get_section_songs, in the order they should end up.'),
|
|
113
142
|
targetSongId: z
|
|
114
143
|
.string()
|
|
115
144
|
.optional()
|
|
116
|
-
.describe('place the moved songs after this song _id; omit for
|
|
145
|
+
.describe('place the moved songs directly after this song _id; omit for the top'),
|
|
117
146
|
confirmToken: confirmTokenParam,
|
|
118
147
|
}),
|
|
119
148
|
}, async ({ eventId, sectionId, sourceSongIds, targetSongId, confirmToken }, ctx) => {
|
|
120
|
-
|
|
149
|
+
if (new Set(sourceSongIds).size !== sourceSongIds.length) {
|
|
150
|
+
throw new McpToolError('sourceSongIds contains a duplicate.', { hint: 'List each song once.' });
|
|
151
|
+
}
|
|
152
|
+
if (targetSongId !== undefined && sourceSongIds.includes(targetSongId)) {
|
|
153
|
+
throw new McpToolError('targetSongId is also one of the songs being moved.', {
|
|
154
|
+
hint: 'Pick a target outside sourceSongIds.',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const [perms, sections, songs] = await Promise.all([
|
|
158
|
+
fetchEventPermissions(client, eventId),
|
|
159
|
+
fetchSections(client, eventId),
|
|
160
|
+
fetchSectionSongs(client, eventId, sectionId),
|
|
161
|
+
]);
|
|
162
|
+
const section = findSection(sections, sectionId);
|
|
163
|
+
// Vibo refuses a host's reorder with "Action is not allowed for user" when
|
|
164
|
+
// the section's "hosts can order songs" is off — which it is for every
|
|
165
|
+
// section a host creates (measured live; a host's attempt to turn it on
|
|
166
|
+
// is accepted and ignored). The web app hides drag handles there too.
|
|
167
|
+
if (isHost(perms) && section.settings?.canHostsOrderSongs === false) {
|
|
168
|
+
throw new McpToolError(`The DJ has turned off host song ordering for "${section.name}", so Vibo refuses reorders from hosts there. Nothing was sent.`, {
|
|
169
|
+
hint: "It's the section's canHostsOrderSongs setting. Vibo turns it off for sections a host creates, and only the DJ " +
|
|
170
|
+
'can turn it on (a host update is ignored). Ask the DJ, or leave a comment on the song instead.',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
assertSongsInSection([...sourceSongIds, ...(targetSongId ? [targetSongId] : [])], songs);
|
|
174
|
+
const { moves } = planMoves(songs.map((s) => s._id), sourceSongIds, targetSongId ?? null);
|
|
175
|
+
if (moves.length === 0) {
|
|
176
|
+
return minifiedResult({ changed: false, message: 'Those songs are already in that order.' });
|
|
177
|
+
}
|
|
178
|
+
const calls = moves.map((m) => ({ sourceSongIds: [m.source], targetSongId: m.target }));
|
|
121
179
|
const gate = await confirmWrite(ctx, {
|
|
122
180
|
tool: 'vibo_reorder_songs',
|
|
123
181
|
mutation: 'reorderSongsBatch',
|
|
124
182
|
message: 'Review and confirm this reorder:',
|
|
125
183
|
confirmToken,
|
|
126
184
|
target: sectionId,
|
|
127
|
-
willSend:
|
|
185
|
+
willSend: { eventId, sectionId, calls },
|
|
128
186
|
});
|
|
129
187
|
if (gate)
|
|
130
188
|
return gate;
|
|
131
|
-
const
|
|
132
|
-
|
|
189
|
+
for (const c of calls) {
|
|
190
|
+
await client.gql(REORDER_SONGS, { eventId, sectionId, ...c });
|
|
191
|
+
}
|
|
192
|
+
return minifiedResult({ changed: true, callsSent: calls.length });
|
|
133
193
|
});
|
|
134
194
|
}
|
package/dist/tools/songs.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { minifiedResult, confirmTokenParam, toolAnnotations } from '@chrischall/mcp-utils';
|
|
2
|
+
import { McpToolError, minifiedResult, confirmTokenParam, toolAnnotations } from '@chrischall/mcp-utils';
|
|
3
3
|
import { viewArg, viewResponse } from '../view.js';
|
|
4
4
|
import { GET_SECTION_SONGS, SEARCH_SONGS, ADD_SONG_TO_SECTION, TOGGLE_LIKE } from '../gql.js';
|
|
5
5
|
import { annotateSearchResults } from '../song-search.js';
|
|
6
6
|
import { limitSchema, skipSchema, pagination, confirmWrite, CONFIRM_NOTE } from './shared.js';
|
|
7
|
+
import { fetchSectionSongs } from './lookups.js';
|
|
8
|
+
/**
|
|
9
|
+
* Waits before each post-add re-read. Vibo's re-reads can be briefly stale
|
|
10
|
+
* (CLAUDE.md, "Write-verification re-reads can be cached"), so one miss is not
|
|
11
|
+
* proof; three spread over ~2s is. Exported so tests can zero them.
|
|
12
|
+
*/
|
|
13
|
+
export const addVerifyDelaysMs = [0, 700, 1500];
|
|
14
|
+
const sleep = (ms) => (ms > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve());
|
|
7
15
|
export function registerSongTools(server, client) {
|
|
8
16
|
server.registerTool('vibo_get_section_songs', {
|
|
9
17
|
description: "List the songs requested in a section, with who added each, like counts, must-play / do-not-play flags, comments, and streaming links. Sort by likesCount, createdAt, or title.",
|
|
@@ -95,7 +103,9 @@ export function registerSongTools(server, client) {
|
|
|
95
103
|
'include viboSongId/title/artist when known). Before adding, check that result\'s ' +
|
|
96
104
|
'`quality.confidence`: adding a `likely-not-original` result puts a cover, karaoke ' +
|
|
97
105
|
'track or junk-metadata re-upload in front of a live DJ. If nothing looks original, ' +
|
|
98
|
-
'report the closest matches back rather than adding a best guess. ' +
|
|
106
|
+
'report the closest matches back rather than adding a best guess. After adding, re-reads the section and ' +
|
|
107
|
+
'reports an error if the song is not actually there (Vibo has answered added:true without adding). ' +
|
|
108
|
+
CONFIRM_NOTE,
|
|
99
109
|
annotations: toolAnnotations({ title: 'Add song to Vibo section', readOnly: false, destructive: false }),
|
|
100
110
|
inputSchema: z.object({
|
|
101
111
|
eventId: z.string().describe('Event id.'),
|
|
@@ -130,7 +140,38 @@ export function registerSongTools(server, client) {
|
|
|
130
140
|
sectionId,
|
|
131
141
|
payload,
|
|
132
142
|
});
|
|
133
|
-
|
|
143
|
+
const res = data.addSongToSection ?? {};
|
|
144
|
+
if (res.added === false) {
|
|
145
|
+
throw new McpToolError('Vibo did not add the song (added: false).', {
|
|
146
|
+
hint: 'It may already be in this section, be on the do-not-play list, or the section may be full. ' +
|
|
147
|
+
'Check with vibo_get_section_songs.',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
// Don't trust added:true alone — confirm the song is really in the section.
|
|
151
|
+
// With no id to match on, say so rather than claim a failure: a false
|
|
152
|
+
// "NOT added" invites a retry that would add the song twice.
|
|
153
|
+
if (!res.songId && !viboSongId) {
|
|
154
|
+
return minifiedResult({
|
|
155
|
+
...res,
|
|
156
|
+
verified: false,
|
|
157
|
+
note: 'Vibo returned no songId and no viboSongId was passed, so the add could not be checked by re-reading. Check with vibo_get_section_songs before retrying.',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const isOurs = (s) => (!!res.songId && s._id === res.songId) || (!!viboSongId && s.viboSongId === viboSongId);
|
|
161
|
+
let found;
|
|
162
|
+
for (const delay of addVerifyDelaysMs) {
|
|
163
|
+
await sleep(delay);
|
|
164
|
+
found = (await fetchSectionSongs(client, eventId, sectionId)).find(isOurs);
|
|
165
|
+
if (found)
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
if (!found) {
|
|
169
|
+
throw new McpToolError('Vibo answered added:true, but the song is not in the section when re-read, so it was NOT added.', {
|
|
170
|
+
hint: 'Retry once; if it still fails, add it in the Vibo app. Details: ' +
|
|
171
|
+
JSON.stringify({ songId: res.songId ?? null, viboSongId: viboSongId ?? null, totalCount: res.totalCount }),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return minifiedResult({ ...res, verified: true, song: found });
|
|
134
175
|
});
|
|
135
176
|
server.registerTool('vibo_toggle_song_like', {
|
|
136
177
|
description: 'Like or unlike a song in a section. ' + CONFIRM_NOTE,
|
package/dist/version.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
// literal on the line carrying the release marker; every manifest and the MCP
|
|
3
3
|
// server banner import VERSION from here, so there is exactly one place to keep
|
|
4
4
|
// in sync (and one release-please extra-files entry).
|
|
5
|
-
export const VERSION = '2.
|
|
5
|
+
export const VERSION = '2.4.0'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibo-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/vibo-mcp",
|
|
5
5
|
"description": "Vibo (vibodj.com) MCP server for Claude — host/couple event music planning & management. Developed and maintained by AI (Claude Code).",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@chrischall/mcp-utils": "^2.
|
|
50
|
-
"@fetchproxy/bootstrap": "^3.
|
|
49
|
+
"@chrischall/mcp-utils": "^2.8.0",
|
|
50
|
+
"@fetchproxy/bootstrap": "^3.4.1",
|
|
51
51
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
52
52
|
"dotenv": "^18.0.1",
|
|
53
53
|
"zod": "^4.6.5"
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/vibo-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.4.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "vibo-mcp",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.4.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
package/skills/vibo-mcp/SKILL.md
CHANGED
|
@@ -40,7 +40,9 @@ Pick one:
|
|
|
40
40
|
- **Captured token (for Apple/Google/Facebook accounts):** set
|
|
41
41
|
`VIBO_ACCESS_TOKEN` (and `VIBO_REFRESH_TOKEN`) with values captured from a
|
|
42
42
|
signed-in `web.vibodj.com` session — no password needed.
|
|
43
|
-
- **Browser capture (SSO, automatic):** with the
|
|
43
|
+
- **Browser capture (SSO, automatic):** with the ContextMint Bridge browser extension
|
|
44
|
+
(https://github.com/nullnet-app/contextmint-bridge/releases — the fetchproxy
|
|
45
|
+
extension renamed, same maintainer, public source; Chrome only for now)
|
|
44
46
|
installed and yourself signed into https://web.vibodj.com, run
|
|
45
47
|
`vibo_capture_session` once — it grabs the token from your tab (approve the
|
|
46
48
|
pair code), saves it to `~/.vibo-mcp/session.json`, and reuses it thereafter.
|
|
@@ -76,8 +78,10 @@ fresh preview and token to re-approve), and a reused one as `TOKEN_REUSED`.
|
|
|
76
78
|
`MCP_CONFIRM_MODE` (`ask-user` default / `auto` / `refuse`) controls this flow.
|
|
77
79
|
|
|
78
80
|
- `vibo_add_song_to_section` — add a searched song to a section.
|
|
79
|
-
- `vibo_remove_song_from_section`
|
|
80
|
-
- `
|
|
81
|
+
- `vibo_remove_song_from_section` — every id is checked against the section first; unknown ids are listed and nothing is sent.
|
|
82
|
+
- `vibo_move_song` / `vibo_reorder_songs` — reorder places `sourceSongIds` directly after `targetSongId` (omit it for the top). A host needs the section's "hosts can order songs" setting on — Vibo turns it off for sections a host creates, and only the DJ can turn it on.
|
|
83
|
+
- `vibo_update_song` — mark must-play / do-not-play, or set a comment (at most 90 characters; an emoji counts as 2).
|
|
84
|
+
- `vibo_add_song_to_section` re-reads the section after adding and errors if the song isn't actually there.
|
|
81
85
|
- `vibo_toggle_song_like` — like/unlike a song.
|
|
82
86
|
- `vibo_comment_on_song` / `vibo_comment_on_section` (+ delete) — leave the DJ notes.
|
|
83
87
|
- `vibo_import_playlist_to_section` — pull tracks from a connected Spotify/Apple playlist.
|
|
@@ -86,6 +90,9 @@ fresh preview and token to re-approve), and a reused one as `TOKEN_REUSED`.
|
|
|
86
90
|
- `vibo_create_event_contact` — add a host/guest contact.
|
|
87
91
|
- `vibo_invite_users` / `vibo_change_user_role` / `vibo_remove_user` — manage who's on the event.
|
|
88
92
|
- `vibo_update_section` — edit a section's name, time, or note.
|
|
93
|
+
- `vibo_create_section` — add a section (name ≤ 45 characters; `visibility` host/public; optional time and note — Vibo drops a host's description) and place it with `afterSectionId` or `position`. Returns the new `_id`.
|
|
94
|
+
- `vibo_delete_section` — delete a section; the preview shows its name, song count and answered questions. `dontPlay`/`headline` sections need `force: true`.
|
|
95
|
+
- `vibo_reorder_sections` — move sections to directly after `targetSectionId` (omit for the start).
|
|
89
96
|
- `vibo_answer_question` — answer a planning question (text / option ids / link / image+file uploads).
|
|
90
97
|
- `vibo_set_profile_photo` — set your profile photo from a local image in the upload directory (`VIBO_UPLOAD_DIR`, default `~/Downloads/vibo-mcp`).
|
|
91
98
|
- `vibo_capture_session` — capture your login from a signed-in browser tab (SSO accounts).
|
|
@@ -94,7 +101,7 @@ fresh preview and token to re-approve), and a reused one as `TOKEN_REUSED`.
|
|
|
94
101
|
|
|
95
102
|
## Response shape (`view`)
|
|
96
103
|
|
|
97
|
-
**Six of this server's
|
|
104
|
+
**Six of this server's 42 tools take `view: "compact" | "full"`**, and on every
|
|
98
105
|
one of them **`compact` is the DEFAULT**. You get the slim rung without asking.
|
|
99
106
|
|
|
100
107
|
They are exactly the six reads whose GraphQL document asks Vibo for media:
|