vibo-mcp 1.7.0 → 1.8.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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "MCP server for Vibo (vibodj.com) — plan & manage event music, song requests, ideas, guests, and playlists via natural language",
10
- "version": "1.7.0"
10
+ "version": "1.8.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "Vibo",
16
16
  "source": "./",
17
17
  "description": "MCP server for Vibo — browse & manage events, timeline, songs, the DJ song ideas/questions, guests, and exports to Spotify/Apple Music",
18
- "version": "1.7.0",
18
+ "version": "1.8.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vibo-mcp",
3
3
  "displayName": "Vibo",
4
- "version": "1.7.0",
4
+ "version": "1.8.0",
5
5
  "description": "MCP server for Vibo (vibodj.com) — plan & manage event music, song requests, ideas, guests, and playlists via natural language",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/bundle.js CHANGED
@@ -35790,7 +35790,7 @@ function toolAnnotations(opts = {}) {
35790
35790
  }
35791
35791
 
35792
35792
  // src/version.ts
35793
- var VERSION = "1.7.0";
35793
+ var VERSION = "1.8.0";
35794
35794
 
35795
35795
  // src/client.ts
35796
35796
  import { dirname as dirname2, join as join3 } from "path";
@@ -36569,17 +36569,27 @@ var UPLOAD_USER_PHOTO = `
36569
36569
  }
36570
36570
  `;
36571
36571
 
36572
+ // src/view.ts
36573
+ var VIBO_VIEWS = ["compact", "full"];
36574
+ var NOTE = `compact strips image/avatar URLs from the response; "full" returns Vibo's payload untouched. No field projection: this server has no verified record of which Vibo fields matter, and inventing one would risk dropping a field a caller needs.`;
36575
+ var viewArg = () => viewParam(VIBO_VIEWS, { note: NOTE });
36576
+ function viewResponse(view, data) {
36577
+ const rung = resolveView(view, VIBO_VIEWS);
36578
+ return minifiedResult(rung === "compact" ? stripMediaUrls(data) : data);
36579
+ }
36580
+
36572
36581
  // src/tools/profile.ts
36573
36582
  function registerProfileTools(server, client2) {
36574
36583
  server.registerTool(
36575
36584
  "vibo_get_me",
36576
36585
  {
36577
36586
  description: "Get the signed-in Vibo user's profile (id, name, email, phone, locale, and whether Spotify/Apple Music are connected). Use the returned _id to recognize your own songs/contacts.",
36578
- annotations: toolAnnotations({ title: "Get my Vibo profile", readOnly: true })
36587
+ annotations: toolAnnotations({ title: "Get my Vibo profile", readOnly: true }),
36588
+ inputSchema: { view: viewArg() }
36579
36589
  },
36580
- async () => {
36590
+ async ({ view }) => {
36581
36591
  const data = await client2.gql(GET_ME);
36582
- return minifiedResult(data.me);
36592
+ return viewResponse(view, data.me);
36583
36593
  }
36584
36594
  );
36585
36595
  server.registerTool(
@@ -36745,15 +36755,6 @@ function registerSectionTools(server, client2) {
36745
36755
  );
36746
36756
  }
36747
36757
 
36748
- // src/view.ts
36749
- var VIBO_VIEWS = ["compact", "full"];
36750
- var NOTE = `compact strips image/avatar URLs from the response; "full" returns Vibo's payload untouched. No field projection: this server has no verified record of which Vibo fields matter, and inventing one would risk dropping a field a caller needs.`;
36751
- var viewArg = () => viewParam(VIBO_VIEWS, { note: NOTE });
36752
- function viewResponse(view, data) {
36753
- const rung = resolveView(view, VIBO_VIEWS);
36754
- return minifiedResult(rung === "compact" ? stripMediaUrls(data) : data);
36755
- }
36756
-
36757
36758
  // src/song-search.ts
36758
36759
  var NON_ORIGINAL_MARKERS = [
36759
36760
  "karaoke",
@@ -36896,11 +36897,12 @@ function registerSongTools(server, client2) {
36896
36897
  isFlagged: external_exports.boolean().optional().describe("Filter to do-not-play / flagged songs."),
36897
36898
  sortField: external_exports.enum(["likesCount", "createdAt", "title"]).optional(),
36898
36899
  sortDirection: external_exports.enum(["asc", "desc"]).optional(),
36900
+ view: viewArg(),
36899
36901
  limit: limitSchema,
36900
36902
  skip: skipSchema
36901
36903
  }
36902
36904
  },
36903
- async ({ eventId, sectionId, q, isMustPlay, isFlagged, sortField, sortDirection, limit, skip }) => {
36905
+ async ({ eventId, sectionId, q, isMustPlay, isFlagged, sortField, sortDirection, limit, skip, view }) => {
36904
36906
  const filter = {};
36905
36907
  if (q !== void 0) filter.q = q;
36906
36908
  if (isMustPlay !== void 0) filter.isMustPlay = isMustPlay;
@@ -36913,7 +36915,7 @@ function registerSongTools(server, client2) {
36913
36915
  ...sortField ? { sort: { field: sortField, direction: sortDirection ?? "desc" } } : {}
36914
36916
  };
36915
36917
  const data = await client2.gql(GET_SECTION_SONGS, variables);
36916
- return minifiedResult(data.getSectionSongs);
36918
+ return viewResponse(view, data.getSectionSongs);
36917
36919
  }
36918
36920
  );
36919
36921
  server.registerTool(
@@ -37100,15 +37102,16 @@ function registerNotificationTools(server, client2) {
37100
37102
  description: "List your Vibo notifications (song additions, comments, DJ updates, etc.) with read state and linked event/section ids.",
37101
37103
  annotations: toolAnnotations({ title: "List Vibo notifications", readOnly: true }),
37102
37104
  inputSchema: {
37105
+ view: viewArg(),
37103
37106
  limit: limitSchema,
37104
37107
  skip: skipSchema
37105
37108
  }
37106
37109
  },
37107
- async ({ limit, skip }) => {
37110
+ async ({ limit, skip, view }) => {
37108
37111
  const data = await client2.gql(GET_NOTIFICATIONS, {
37109
37112
  pagination: pagination(limit, skip)
37110
37113
  });
37111
- return minifiedResult(data.getNotifications);
37114
+ return viewResponse(view, data.getNotifications);
37112
37115
  }
37113
37116
  );
37114
37117
  server.registerTool(
@@ -37497,18 +37500,19 @@ function registerIdeasTools(server, client2) {
37497
37500
  eventId: external_exports.string().describe("Event id."),
37498
37501
  sectionId: external_exports.string().describe("Section id (from vibo_list_sections)."),
37499
37502
  songIdeasId: external_exports.string().describe("The _id from vibo_list_section_song_ideas."),
37503
+ view: viewArg(),
37500
37504
  limit: limitSchema,
37501
37505
  skip: skipSchema
37502
37506
  }
37503
37507
  },
37504
- async ({ eventId, sectionId, songIdeasId, limit, skip }) => {
37508
+ async ({ eventId, sectionId, songIdeasId, limit, skip, view }) => {
37505
37509
  const data = await client2.gql(LIST_SONG_IDEAS_SONGS, {
37506
37510
  eventId,
37507
37511
  sectionId,
37508
37512
  songIdeasId,
37509
37513
  pagination: pagination(limit, skip)
37510
37514
  });
37511
- return minifiedResult(data.getEventSectionSongIdeasSongs);
37515
+ return viewResponse(view, data.getEventSectionSongIdeasSongs);
37512
37516
  }
37513
37517
  );
37514
37518
  }
@@ -37559,11 +37563,12 @@ function registerCollaborationTools(server, client2) {
37559
37563
  inputSchema: {
37560
37564
  eventId: external_exports.string().describe("Event id."),
37561
37565
  usersType: external_exports.enum(["host", "guest"]).optional().describe("Filter to only hosts or only guests."),
37566
+ view: viewArg(),
37562
37567
  limit: limitSchema.describe("Max items to return (default 20). Applies per group when usersType is omitted."),
37563
37568
  skip: skipSchema
37564
37569
  }
37565
37570
  },
37566
- async ({ eventId, usersType, limit, skip }) => {
37571
+ async ({ eventId, usersType, limit, skip, view }) => {
37567
37572
  const page = pagination(limit, skip);
37568
37573
  if (usersType) {
37569
37574
  const data = await client2.gql(LIST_EVENT_USERS, {
@@ -37571,13 +37576,13 @@ function registerCollaborationTools(server, client2) {
37571
37576
  usersType,
37572
37577
  pagination: page
37573
37578
  });
37574
- return minifiedResult({ ...data.eventUsers, usersType });
37579
+ return viewResponse(view, { ...data.eventUsers, usersType });
37575
37580
  }
37576
37581
  const [hosts, guests] = await Promise.all([
37577
37582
  client2.gql(LIST_EVENT_USERS, { eventId, usersType: "host", pagination: page }),
37578
37583
  client2.gql(LIST_EVENT_USERS, { eventId, usersType: "guest", pagination: page })
37579
37584
  ]);
37580
- return minifiedResult({
37585
+ return viewResponse(view, {
37581
37586
  hosts: hosts.eventUsers.users,
37582
37587
  guests: guests.eventUsers.users,
37583
37588
  hostsCount: hosts.eventUsers.totalCount,
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { minifiedResult, schemaConfirm, toolAnnotations } from '@chrischall/mcp-utils';
3
3
  import { LIST_EVENT_USERS, INVITE_USERS, CHANGE_USER_ROLE, REMOVE_USER } from '../gql.js';
4
4
  import { limitSchema, skipSchema, pagination, previewResult } from './shared.js';
5
+ import { viewArg, viewResponse } from '../view.js';
5
6
  export function registerCollaborationTools(server, client) {
6
7
  server.registerTool('vibo_list_event_users', {
7
8
  description: "List the hosts and guests on an event. With no usersType, returns both groups merged ({hosts, guests, hostsCount, guestsCount}) and `limit`/`skip` apply per group; with usersType, returns that one group's page.",
@@ -9,10 +10,11 @@ export function registerCollaborationTools(server, client) {
9
10
  inputSchema: {
10
11
  eventId: z.string().describe('Event id.'),
11
12
  usersType: z.enum(['host', 'guest']).optional().describe('Filter to only hosts or only guests.'),
13
+ view: viewArg(),
12
14
  limit: limitSchema.describe('Max items to return (default 20). Applies per group when usersType is omitted.'),
13
15
  skip: skipSchema,
14
16
  },
15
- }, async ({ eventId, usersType, limit, skip }) => {
17
+ }, async ({ eventId, usersType, limit, skip, view }) => {
16
18
  const page = pagination(limit, skip);
17
19
  if (usersType) {
18
20
  const data = await client.gql(LIST_EVENT_USERS, {
@@ -20,7 +22,7 @@ export function registerCollaborationTools(server, client) {
20
22
  usersType,
21
23
  pagination: page,
22
24
  });
23
- return minifiedResult({ ...data.eventUsers, usersType });
25
+ return viewResponse(view, { ...data.eventUsers, usersType });
24
26
  }
25
27
  // The API returns nothing unless usersType is set, so fetch both groups
26
28
  // and merge for the intuitive "everyone on the event" listing.
@@ -28,7 +30,10 @@ export function registerCollaborationTools(server, client) {
28
30
  client.gql(LIST_EVENT_USERS, { eventId, usersType: 'host', pagination: page }),
29
31
  client.gql(LIST_EVENT_USERS, { eventId, usersType: 'guest', pagination: page }),
30
32
  ]);
31
- return minifiedResult({
33
+ // BOTH exits go through the rung. The merged branch is the one that runs
34
+ // when `usersType` is omitted — the default call — so honouring `view` on
35
+ // only the filtered branch would leave the common path paying full price.
36
+ return viewResponse(view, {
32
37
  hosts: hosts.eventUsers.users,
33
38
  guests: guests.eventUsers.users,
34
39
  hostsCount: hosts.eventUsers.totalCount,
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
3
3
  import { LIST_SECTION_SONG_IDEAS, LIST_SONG_IDEAS_SONGS } from '../gql.js';
4
4
  import { limitSchema, skipSchema, pagination } from './shared.js';
5
+ import { viewArg, viewResponse } from '../view.js';
5
6
  export function registerIdeasTools(server, client) {
6
7
  server.registerTool('vibo_list_section_song_ideas', {
7
8
  description: "List the DJ's suggested song-idea collections for a section (each with a title, songsCount and _id). Use a song-ideas _id with vibo_list_song_ideas_songs to see the suggested songs, then add the ones you like with vibo_add_song_to_section.",
@@ -27,16 +28,17 @@ export function registerIdeasTools(server, client) {
27
28
  eventId: z.string().describe('Event id.'),
28
29
  sectionId: z.string().describe('Section id (from vibo_list_sections).'),
29
30
  songIdeasId: z.string().describe('The _id from vibo_list_section_song_ideas.'),
31
+ view: viewArg(),
30
32
  limit: limitSchema,
31
33
  skip: skipSchema,
32
34
  },
33
- }, async ({ eventId, sectionId, songIdeasId, limit, skip }) => {
35
+ }, async ({ eventId, sectionId, songIdeasId, limit, skip, view }) => {
34
36
  const data = await client.gql(LIST_SONG_IDEAS_SONGS, {
35
37
  eventId,
36
38
  sectionId,
37
39
  songIdeasId,
38
40
  pagination: pagination(limit, skip),
39
41
  });
40
- return minifiedResult(data.getEventSectionSongIdeasSongs);
42
+ return viewResponse(view, data.getEventSectionSongIdeasSongs);
41
43
  });
42
44
  }
@@ -2,19 +2,21 @@ import { z } from 'zod';
2
2
  import { McpToolError, minifiedResult, schemaConfirm, toolAnnotations } from '@chrischall/mcp-utils';
3
3
  import { GET_NOTIFICATIONS, GET_NOTIFICATIONS_COUNT, MARK_AS_READ } from '../gql.js';
4
4
  import { limitSchema, skipSchema, pagination, previewResult } from './shared.js';
5
+ import { viewArg, viewResponse } from '../view.js';
5
6
  export function registerNotificationTools(server, client) {
6
7
  server.registerTool('vibo_list_notifications', {
7
8
  description: 'List your Vibo notifications (song additions, comments, DJ updates, etc.) with read state and linked event/section ids.',
8
9
  annotations: toolAnnotations({ title: 'List Vibo notifications', readOnly: true }),
9
10
  inputSchema: {
11
+ view: viewArg(),
10
12
  limit: limitSchema,
11
13
  skip: skipSchema,
12
14
  },
13
- }, async ({ limit, skip }) => {
15
+ }, async ({ limit, skip, view }) => {
14
16
  const data = await client.gql(GET_NOTIFICATIONS, {
15
17
  pagination: pagination(limit, skip),
16
18
  });
17
- return minifiedResult(data.getNotifications);
19
+ return viewResponse(view, data.getNotifications);
18
20
  });
19
21
  server.registerTool('vibo_get_notifications_count', {
20
22
  description: 'Get the count of unread Vibo notifications.',
@@ -1,12 +1,14 @@
1
1
  import { minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
2
2
  import { GET_ME } from '../gql.js';
3
+ import { viewArg, viewResponse } from '../view.js';
3
4
  export function registerProfileTools(server, client) {
4
5
  server.registerTool('vibo_get_me', {
5
6
  description: "Get the signed-in Vibo user's profile (id, name, email, phone, locale, and whether Spotify/Apple Music are connected). Use the returned _id to recognize your own songs/contacts.",
6
7
  annotations: toolAnnotations({ title: 'Get my Vibo profile', readOnly: true }),
7
- }, async () => {
8
+ inputSchema: { view: viewArg() },
9
+ }, async ({ view }) => {
8
10
  const data = await client.gql(GET_ME);
9
- return minifiedResult(data.me);
11
+ return viewResponse(view, data.me);
10
12
  });
11
13
  server.registerTool('vibo_healthcheck', {
12
14
  description: 'Verify connectivity and authentication to the Vibo API by fetching the current user. Returns ok:true with your account id when credentials work.',
@@ -16,10 +16,11 @@ export function registerSongTools(server, client) {
16
16
  isFlagged: z.boolean().optional().describe('Filter to do-not-play / flagged songs.'),
17
17
  sortField: z.enum(['likesCount', 'createdAt', 'title']).optional(),
18
18
  sortDirection: z.enum(['asc', 'desc']).optional(),
19
+ view: viewArg(),
19
20
  limit: limitSchema,
20
21
  skip: skipSchema,
21
22
  },
22
- }, async ({ eventId, sectionId, q, isMustPlay, isFlagged, sortField, sortDirection, limit, skip }) => {
23
+ }, async ({ eventId, sectionId, q, isMustPlay, isFlagged, sortField, sortDirection, limit, skip, view }) => {
23
24
  const filter = {};
24
25
  if (q !== undefined)
25
26
  filter.q = q;
@@ -35,7 +36,7 @@ export function registerSongTools(server, client) {
35
36
  ...(sortField ? { sort: { field: sortField, direction: sortDirection ?? 'desc' } } : {}),
36
37
  };
37
38
  const data = await client.gql(GET_SECTION_SONGS, variables);
38
- return minifiedResult(data.getSectionSongs);
39
+ return viewResponse(view, data.getSectionSongs);
39
40
  });
40
41
  server.registerTool('vibo_search_songs', {
41
42
  description: 'Search for songs to add to a section. ALWAYS query as "<Artist> - <Title>" with a ' +
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 = '1.7.0'; // x-release-please-version
5
+ export const VERSION = '1.8.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibo-mcp",
3
- "version": "1.7.0",
3
+ "version": "1.8.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>",
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": "1.7.0",
9
+ "version": "1.8.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "vibo-mcp",
14
- "version": "1.7.0",
14
+ "version": "1.8.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -85,6 +85,61 @@ you get a dry-run preview of exactly what would be sent.
85
85
  - `vibo_mark_notifications_read`.
86
86
  - `vibo_export_event_to_spotify` / `vibo_export_event_to_apple_music`.
87
87
 
88
+ ## Response shape (`view`)
89
+
90
+ **Six of this server's 39 tools take `view: "compact" | "full"`**, and on every
91
+ one of them **`compact` is the DEFAULT**. You get the slim rung without asking.
92
+
93
+ They are exactly the six reads whose GraphQL document asks Vibo for media:
94
+
95
+ | Tool | What compact drops |
96
+ | --- | --- |
97
+ | `vibo_search_songs` | `thumbnails { s180x180 original }` per track |
98
+ | `vibo_get_section_songs` | `thumbnails { s180x180 original }` per song |
99
+ | `vibo_list_song_ideas_songs` | `thumbnails { s180x180 original }` per song |
100
+ | `vibo_list_event_users` | `imageUrl` per person (both exits — merged and filtered) |
101
+ | `vibo_list_notifications` | `imageUrl` per notification |
102
+ | `vibo_get_me` | `imageUrl` |
103
+
104
+ That correspondence is not a coincidence to be maintained by hand: a test in
105
+ `tests/view.test.ts` counts the media selections in `src/gql.ts` and asserts
106
+ the roster of `view`-taking tools matches. Add a seventh selection and it fails.
107
+
108
+ **Compact here is media stripping, not a field projection.** `src/view.ts`
109
+ writes no field list, because this repo holds no captured Vibo payload to
110
+ derive one from honestly; instead it removes keys whose value is a picture,
111
+ which is subtractive and so cannot drop a field nobody knew about.
112
+
113
+ What compact does **not** touch is the point of these tools: `songUrl`, the
114
+ `links` block (`spotify` / `youtube` / `appleMusic`) and the `quality` verdict
115
+ all survive. A streaming link is this server's product, not decoration —
116
+ `vibo_add_song_to_section` cannot add anything without `songUrl`, and the
117
+ quality verdict is computed from how many services carry a track. A `null` link
118
+ survives too: an absent key and a null one are the same to `JSON.parse` but not
119
+ to a reader deciding whether a service was checked.
120
+
121
+ `view: "full"` returns Vibo's payload untouched, cover art included. There is
122
+ deliberately **no `raw` rung**: nothing here re-serialises or normalises the
123
+ GraphQL response, so `full` already IS the upstream payload and a third value
124
+ would silently alias one that exists.
125
+
126
+ ### The other 33 tools have no `view`
127
+
128
+ - **The 24 mutating tools** (every confirm-gated write, plus
129
+ `vibo_capture_session`) answer with a dry-run preview or a receipt — an id,
130
+ a count, a status. Nothing in a receipt is decoration, and slimming one is
131
+ how you lose the field that says what actually happened.
132
+ - **`vibo_healthcheck`** answers with a connectivity/auth diagnostic. It runs
133
+ the same `GET_ME` document as `vibo_get_me` and still takes no rung, because
134
+ it never returns Vibo's user object — it builds `{ok, userId, email}` here.
135
+ A `view` on it would be a parameter that changes nothing.
136
+ - **The 8 remaining reads hand back Vibo's GraphQL payload as it arrived**, and
137
+ their documents select no media, so there is nothing for a rung to remove.
138
+
139
+ A `view` passed to a tool that does not declare one is dropped by zod without a
140
+ warning, so a successful call is never evidence the rung was honoured. Check
141
+ the table above rather than assuming.
142
+
88
143
  ## Typical flow
89
144
 
90
145
  1. `vibo_list_events` → pick an event id.