vibo-mcp 1.7.1 → 1.8.1

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.1"
10
+ "version": "1.8.1"
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.1",
18
+ "version": "1.8.1",
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.1",
4
+ "version": "1.8.1",
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
@@ -13450,7 +13450,7 @@ ${content.join("\n")}
13450
13450
  var version = {
13451
13451
  major: 4,
13452
13452
  minor: 5,
13453
- patch: 2
13453
+ patch: 4
13454
13454
  };
13455
13455
 
13456
13456
  // node_modules/zod/v4/core/schemas.js
@@ -15743,22 +15743,99 @@ function isRecursive(inst, stack) {
15743
15743
  result = true;
15744
15744
  };
15745
15745
  const def = inst._zod.def;
15746
- if (def.type === "lazy") {
15747
- check2(inst._zod.innerType);
15748
- } else {
15749
- const shape = def.shape;
15750
- if (shape)
15751
- for (const key of Reflect.ownKeys(shape))
15752
- check2(shape[key]);
15753
- for (const key in def) {
15754
- const value = def[key];
15755
- if (!value || typeof value !== "object")
15756
- continue;
15757
- if (value._zod)
15758
- check2(value);
15759
- else if (Array.isArray(value))
15760
- for (const el of value)
15761
- check2(el);
15746
+ const kind = def.type;
15747
+ switch (kind) {
15748
+ case "object": {
15749
+ for (const key of Reflect.ownKeys(def.shape))
15750
+ check2(def.shape[key]);
15751
+ check2(def.catchall);
15752
+ break;
15753
+ }
15754
+ case "array":
15755
+ check2(def.element);
15756
+ break;
15757
+ case "tuple":
15758
+ for (const el of def.items)
15759
+ check2(el);
15760
+ check2(def.rest);
15761
+ break;
15762
+ case "record":
15763
+ case "map":
15764
+ check2(def.keyType);
15765
+ check2(def.valueType);
15766
+ break;
15767
+ case "set":
15768
+ check2(def.valueType);
15769
+ break;
15770
+ case "union":
15771
+ for (const el of def.options)
15772
+ check2(el);
15773
+ break;
15774
+ case "intersection":
15775
+ check2(def.left);
15776
+ check2(def.right);
15777
+ break;
15778
+ case "optional":
15779
+ case "nullable":
15780
+ case "default":
15781
+ case "prefault":
15782
+ case "catch":
15783
+ case "readonly":
15784
+ case "nonoptional":
15785
+ case "promise":
15786
+ case "success":
15787
+ check2(def.innerType);
15788
+ break;
15789
+ case "pipe":
15790
+ check2(def.in);
15791
+ check2(def.out);
15792
+ break;
15793
+ case "function":
15794
+ check2(def.input);
15795
+ check2(def.output);
15796
+ break;
15797
+ // reading `_zod.innerType` resolves the getter once and caches it
15798
+ case "lazy":
15799
+ check2(inst._zod.innerType);
15800
+ break;
15801
+ // a leaf by choice: `parts` are regex fragments, not data positions
15802
+ case "template_literal":
15803
+ // leaves
15804
+ case "string":
15805
+ case "number":
15806
+ case "int":
15807
+ case "boolean":
15808
+ case "bigint":
15809
+ case "symbol":
15810
+ case "undefined":
15811
+ case "null":
15812
+ case "void":
15813
+ case "never":
15814
+ case "any":
15815
+ case "unknown":
15816
+ case "date":
15817
+ case "nan":
15818
+ case "enum":
15819
+ case "literal":
15820
+ case "file":
15821
+ case "transform":
15822
+ case "custom":
15823
+ break;
15824
+ default: {
15825
+ kind;
15826
+ for (const key in def) {
15827
+ const desc = Object.getOwnPropertyDescriptor(def, key);
15828
+ if (!desc || desc.get)
15829
+ continue;
15830
+ const value = desc.value;
15831
+ if (!value || typeof value !== "object")
15832
+ continue;
15833
+ if (value._zod)
15834
+ check2(value);
15835
+ else if (Array.isArray(value))
15836
+ for (const el of value)
15837
+ check2(el);
15838
+ }
15762
15839
  }
15763
15840
  }
15764
15841
  stack.delete(inst);
@@ -25891,6 +25968,7 @@ function initializeContext(params) {
25891
25968
  cycles: params?.cycles ?? "ref",
25892
25969
  reused: params?.reused ?? "inline",
25893
25970
  intersections: [],
25971
+ deferred: [],
25894
25972
  external: params?.external ?? void 0
25895
25973
  };
25896
25974
  }
@@ -26239,6 +26317,8 @@ function finalize(ctx, schema) {
26239
26317
  compactTypeUnion(entry[1].def ?? entry[1].schema);
26240
26318
  }
26241
26319
  }
26320
+ for (const rewrite of ctx.deferred)
26321
+ rewrite();
26242
26322
  if (ctx.intersections.length) {
26243
26323
  const carriers = /* @__PURE__ */ new Map();
26244
26324
  for (const seen of ctx.seen.values()) {
@@ -26415,12 +26495,12 @@ var stringProcessor = (schema, ctx, _json, _params) => {
26415
26495
  if (contentEncoding)
26416
26496
  json2.contentEncoding = contentEncoding;
26417
26497
  if (patterns && patterns.size > 0) {
26418
- const regexes = [...patterns];
26419
- if (regexes.length === 1)
26420
- json2.pattern = regexes[0].source;
26421
- else if (regexes.length > 1) {
26498
+ const patternList = [...patterns];
26499
+ if (patternList.length === 1)
26500
+ json2.pattern = patternList[0].source;
26501
+ else if (patternList.length > 1) {
26422
26502
  json2.allOf = [
26423
- ...regexes.map((regex) => ({
26503
+ ...patternList.map((regex) => ({
26424
26504
  ...ctx.target === "draft-07" || ctx.target === "draft-04" || ctx.target === "openapi-3.0" ? { type: "string" } : {},
26425
26505
  pattern: regex.source
26426
26506
  }))
@@ -26764,6 +26844,69 @@ var tupleProcessor = (schema, ctx, _json, params) => {
26764
26844
  if (typeof maximum === "number")
26765
26845
  json2.maxItems = maximum;
26766
26846
  };
26847
+ function stringifyKeyNames(bySchema, json2, visited) {
26848
+ if (json2.$ref) {
26849
+ if (visited.has(json2))
26850
+ return json2;
26851
+ visited.add(json2);
26852
+ const def = bySchema.get(json2)?.def;
26853
+ if (!def)
26854
+ return json2;
26855
+ const inlined = stringifyKeyNames(bySchema, def, visited);
26856
+ return inlined === def ? json2 : inlined;
26857
+ }
26858
+ for (const keyword of ["anyOf", "oneOf"]) {
26859
+ const branches = json2[keyword];
26860
+ if (!Array.isArray(branches))
26861
+ continue;
26862
+ const mapped = branches.map((branch) => stringifyKeyNames(bySchema, branch, visited));
26863
+ if (mapped.some((branch, i) => branch !== branches[i]))
26864
+ json2 = { ...json2, [keyword]: mapped };
26865
+ }
26866
+ const types = Array.isArray(json2.type) ? json2.type : [json2.type];
26867
+ const numericType = !types.includes("string") && types.some((t) => t === "number" || t === "integer");
26868
+ const values = json2.enum ?? (json2.const !== void 0 ? [json2.const] : void 0);
26869
+ if (!numericType && !values?.some((v) => typeof v === "number"))
26870
+ return json2;
26871
+ const { minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, format, id, ...rest } = json2;
26872
+ if (rest.enum)
26873
+ rest.enum = rest.enum.map((v) => typeof v === "number" ? String(v) : v);
26874
+ else if (typeof rest.const === "number")
26875
+ rest.const = String(rest.const);
26876
+ if (!numericType)
26877
+ return rest;
26878
+ rest.type = "string";
26879
+ if (!values)
26880
+ rest.pattern = (types.includes("number") ? number : integer).source;
26881
+ return rest;
26882
+ }
26883
+ var pendingRecords = /* @__PURE__ */ new WeakMap();
26884
+ function rewriteKeyNames(ctx) {
26885
+ const bySchema = /* @__PURE__ */ new Map();
26886
+ for (const entry of ctx.seen.values()) {
26887
+ if (entry.def && !bySchema.has(entry.schema))
26888
+ bySchema.set(entry.schema, entry);
26889
+ }
26890
+ const rewrites = /* @__PURE__ */ new Map();
26891
+ for (const record2 of pendingRecords.get(ctx) ?? []) {
26892
+ const seen = ctx.seen.get(record2);
26893
+ const names = (seen?.def ?? seen?.schema)?.propertyNames;
26894
+ if (!names || names === true || rewrites.has(names))
26895
+ continue;
26896
+ const rewritten = stringifyKeyNames(bySchema, names, /* @__PURE__ */ new Set());
26897
+ if (rewritten !== names)
26898
+ rewrites.set(names, rewritten);
26899
+ }
26900
+ if (!rewrites.size)
26901
+ return;
26902
+ for (const entry of ctx.seen.values()) {
26903
+ for (const carrier of [entry.schema, entry.def]) {
26904
+ const rewritten = carrier && rewrites.get(carrier.propertyNames);
26905
+ if (rewritten)
26906
+ carrier.propertyNames = rewritten;
26907
+ }
26908
+ }
26909
+ }
26767
26910
  var recordProcessor = (schema, ctx, _json, params) => {
26768
26911
  const json2 = _json;
26769
26912
  const def = schema._zod.def;
@@ -26786,6 +26929,13 @@ var recordProcessor = (schema, ctx, _json, params) => {
26786
26929
  ...params,
26787
26930
  path: [...params.path, "propertyNames"]
26788
26931
  });
26932
+ let pending = pendingRecords.get(ctx);
26933
+ if (!pending) {
26934
+ pending = [];
26935
+ pendingRecords.set(ctx, pending);
26936
+ ctx.deferred.push(() => rewriteKeyNames(ctx));
26937
+ }
26938
+ pending.push(schema);
26789
26939
  }
26790
26940
  json2.additionalProperties = process2(def.valueType, ctx, {
26791
26941
  ...params,
@@ -26797,7 +26947,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
26797
26947
  if (keyValues && !def.partial && !omittableOnInput) {
26798
26948
  const validKeyValues = [...keyValues].filter((v) => typeof v === "string" || typeof v === "number");
26799
26949
  if (validKeyValues.length > 0) {
26800
- json2.required = validKeyValues;
26950
+ json2.required = validKeyValues.map(String);
26801
26951
  }
26802
26952
  }
26803
26953
  };
@@ -35790,7 +35940,7 @@ function toolAnnotations(opts = {}) {
35790
35940
  }
35791
35941
 
35792
35942
  // src/version.ts
35793
- var VERSION = "1.7.1";
35943
+ var VERSION = "1.8.1";
35794
35944
 
35795
35945
  // src/client.ts
35796
35946
  import { dirname as dirname2, join as join3 } from "path";
@@ -36569,17 +36719,27 @@ var UPLOAD_USER_PHOTO = `
36569
36719
  }
36570
36720
  `;
36571
36721
 
36722
+ // src/view.ts
36723
+ var VIBO_VIEWS = ["compact", "full"];
36724
+ 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.`;
36725
+ var viewArg = () => viewParam(VIBO_VIEWS, { note: NOTE });
36726
+ function viewResponse(view, data) {
36727
+ const rung = resolveView(view, VIBO_VIEWS);
36728
+ return minifiedResult(rung === "compact" ? stripMediaUrls(data) : data);
36729
+ }
36730
+
36572
36731
  // src/tools/profile.ts
36573
36732
  function registerProfileTools(server, client2) {
36574
36733
  server.registerTool(
36575
36734
  "vibo_get_me",
36576
36735
  {
36577
36736
  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 })
36737
+ annotations: toolAnnotations({ title: "Get my Vibo profile", readOnly: true }),
36738
+ inputSchema: { view: viewArg() }
36579
36739
  },
36580
- async () => {
36740
+ async ({ view }) => {
36581
36741
  const data = await client2.gql(GET_ME);
36582
- return minifiedResult(data.me);
36742
+ return viewResponse(view, data.me);
36583
36743
  }
36584
36744
  );
36585
36745
  server.registerTool(
@@ -36745,15 +36905,6 @@ function registerSectionTools(server, client2) {
36745
36905
  );
36746
36906
  }
36747
36907
 
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
36908
  // src/song-search.ts
36758
36909
  var NON_ORIGINAL_MARKERS = [
36759
36910
  "karaoke",
@@ -36896,11 +37047,12 @@ function registerSongTools(server, client2) {
36896
37047
  isFlagged: external_exports.boolean().optional().describe("Filter to do-not-play / flagged songs."),
36897
37048
  sortField: external_exports.enum(["likesCount", "createdAt", "title"]).optional(),
36898
37049
  sortDirection: external_exports.enum(["asc", "desc"]).optional(),
37050
+ view: viewArg(),
36899
37051
  limit: limitSchema,
36900
37052
  skip: skipSchema
36901
37053
  }
36902
37054
  },
36903
- async ({ eventId, sectionId, q, isMustPlay, isFlagged, sortField, sortDirection, limit, skip }) => {
37055
+ async ({ eventId, sectionId, q, isMustPlay, isFlagged, sortField, sortDirection, limit, skip, view }) => {
36904
37056
  const filter = {};
36905
37057
  if (q !== void 0) filter.q = q;
36906
37058
  if (isMustPlay !== void 0) filter.isMustPlay = isMustPlay;
@@ -36913,7 +37065,7 @@ function registerSongTools(server, client2) {
36913
37065
  ...sortField ? { sort: { field: sortField, direction: sortDirection ?? "desc" } } : {}
36914
37066
  };
36915
37067
  const data = await client2.gql(GET_SECTION_SONGS, variables);
36916
- return minifiedResult(data.getSectionSongs);
37068
+ return viewResponse(view, data.getSectionSongs);
36917
37069
  }
36918
37070
  );
36919
37071
  server.registerTool(
@@ -37100,15 +37252,16 @@ function registerNotificationTools(server, client2) {
37100
37252
  description: "List your Vibo notifications (song additions, comments, DJ updates, etc.) with read state and linked event/section ids.",
37101
37253
  annotations: toolAnnotations({ title: "List Vibo notifications", readOnly: true }),
37102
37254
  inputSchema: {
37255
+ view: viewArg(),
37103
37256
  limit: limitSchema,
37104
37257
  skip: skipSchema
37105
37258
  }
37106
37259
  },
37107
- async ({ limit, skip }) => {
37260
+ async ({ limit, skip, view }) => {
37108
37261
  const data = await client2.gql(GET_NOTIFICATIONS, {
37109
37262
  pagination: pagination(limit, skip)
37110
37263
  });
37111
- return minifiedResult(data.getNotifications);
37264
+ return viewResponse(view, data.getNotifications);
37112
37265
  }
37113
37266
  );
37114
37267
  server.registerTool(
@@ -37497,18 +37650,19 @@ function registerIdeasTools(server, client2) {
37497
37650
  eventId: external_exports.string().describe("Event id."),
37498
37651
  sectionId: external_exports.string().describe("Section id (from vibo_list_sections)."),
37499
37652
  songIdeasId: external_exports.string().describe("The _id from vibo_list_section_song_ideas."),
37653
+ view: viewArg(),
37500
37654
  limit: limitSchema,
37501
37655
  skip: skipSchema
37502
37656
  }
37503
37657
  },
37504
- async ({ eventId, sectionId, songIdeasId, limit, skip }) => {
37658
+ async ({ eventId, sectionId, songIdeasId, limit, skip, view }) => {
37505
37659
  const data = await client2.gql(LIST_SONG_IDEAS_SONGS, {
37506
37660
  eventId,
37507
37661
  sectionId,
37508
37662
  songIdeasId,
37509
37663
  pagination: pagination(limit, skip)
37510
37664
  });
37511
- return minifiedResult(data.getEventSectionSongIdeasSongs);
37665
+ return viewResponse(view, data.getEventSectionSongIdeasSongs);
37512
37666
  }
37513
37667
  );
37514
37668
  }
@@ -37559,11 +37713,12 @@ function registerCollaborationTools(server, client2) {
37559
37713
  inputSchema: {
37560
37714
  eventId: external_exports.string().describe("Event id."),
37561
37715
  usersType: external_exports.enum(["host", "guest"]).optional().describe("Filter to only hosts or only guests."),
37716
+ view: viewArg(),
37562
37717
  limit: limitSchema.describe("Max items to return (default 20). Applies per group when usersType is omitted."),
37563
37718
  skip: skipSchema
37564
37719
  }
37565
37720
  },
37566
- async ({ eventId, usersType, limit, skip }) => {
37721
+ async ({ eventId, usersType, limit, skip, view }) => {
37567
37722
  const page = pagination(limit, skip);
37568
37723
  if (usersType) {
37569
37724
  const data = await client2.gql(LIST_EVENT_USERS, {
@@ -37571,13 +37726,13 @@ function registerCollaborationTools(server, client2) {
37571
37726
  usersType,
37572
37727
  pagination: page
37573
37728
  });
37574
- return minifiedResult({ ...data.eventUsers, usersType });
37729
+ return viewResponse(view, { ...data.eventUsers, usersType });
37575
37730
  }
37576
37731
  const [hosts, guests] = await Promise.all([
37577
37732
  client2.gql(LIST_EVENT_USERS, { eventId, usersType: "host", pagination: page }),
37578
37733
  client2.gql(LIST_EVENT_USERS, { eventId, usersType: "guest", pagination: page })
37579
37734
  ]);
37580
- return minifiedResult({
37735
+ return viewResponse(view, {
37581
37736
  hosts: hosts.eventUsers.users,
37582
37737
  guests: guests.eventUsers.users,
37583
37738
  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.1'; // x-release-please-version
5
+ export const VERSION = '1.8.1'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibo-mcp",
3
- "version": "1.7.1",
3
+ "version": "1.8.1",
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,17 +46,17 @@
46
46
  "typecheck": "tsc -p tsconfig.json --noEmit"
47
47
  },
48
48
  "dependencies": {
49
- "@chrischall/mcp-utils": "^0.23.0",
49
+ "@chrischall/mcp-utils": "^0.26.1",
50
50
  "@fetchproxy/bootstrap": "^2.2.0",
51
- "@modelcontextprotocol/sdk": "^1.29.0",
51
+ "@modelcontextprotocol/sdk": "^1.30.0",
52
52
  "dotenv": "^17.4.0",
53
- "zod": "^4.4.2"
53
+ "zod": "^4.5.4"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/node": "^26.0.0",
57
- "@vitest/coverage-v8": "^4.1.2",
57
+ "@vitest/coverage-v8": "^5.0.0",
58
58
  "esbuild": "^0.28.0",
59
59
  "typescript": "^7.0.2",
60
- "vitest": "^4.1.2"
60
+ "vitest": "^5.0.0"
61
61
  }
62
62
  }
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.1",
9
+ "version": "1.8.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "vibo-mcp",
14
- "version": "1.7.1",
14
+ "version": "1.8.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -87,63 +87,58 @@ you get a dry-run preview of exactly what would be sent.
87
87
 
88
88
  ## Response shape (`view`)
89
89
 
90
- Exactly one of this server's 39 tools takes `view: "compact" | "full"` —
91
- **`vibo_search_songs`** — and on it **`compact` is the DEFAULT**. You get the
92
- slim rung without asking for it.
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.
93
107
 
94
108
  **Compact here is media stripping, not a field projection.** `src/view.ts`
95
109
  writes no field list, because this repo holds no captured Vibo payload to
96
110
  derive one from honestly; instead it removes keys whose value is a picture,
97
111
  which is subtractive and so cannot drop a field nobody knew about.
98
112
 
99
- Why `vibo_search_songs` is the tool that got it: `annotateSearchResults`
100
- SPREADS Vibo's own song objects (`{ ...song, quality }`), so it is a
101
- pass-through with a verdict attached rather than a projection — and the search
102
- query selects `thumbnails { s180x180 original }` per track, so every cover-art
103
- URL in the catalog was coming straight back to the caller. Compact removes
104
- those.
105
-
106
- What compact does **not** touch on that tool is the point of the tool:
107
- `songUrl`, the `links` block (`spotify` / `youtube` / `appleMusic`) and the
108
- `quality` verdict all survive. A streaming link is this server's product, not
109
- decoration — `vibo_add_song_to_section` cannot add anything without `songUrl`,
110
- and the quality verdict is computed from how many services carry a track. A
111
- `null` link survives too: an absent key and a null one are the same to
112
- `JSON.parse` but not to a reader deciding whether a service was checked.
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.
113
120
 
114
121
  `view: "full"` returns Vibo's payload untouched, cover art included. There is
115
122
  deliberately **no `raw` rung**: nothing here re-serialises or normalises the
116
123
  GraphQL response, so `full` already IS the upstream payload and a third value
117
124
  would silently alias one that exists.
118
125
 
119
- ### The other 38 tools have no `view` — and one group of them matters
126
+ ### The other 33 tools have no `view`
120
127
 
121
128
  - **The 24 mutating tools** (every confirm-gated write, plus
122
129
  `vibo_capture_session`) answer with a dry-run preview or a receipt — an id,
123
130
  a count, a status. Nothing in a receipt is decoration, and slimming one is
124
131
  how you lose the field that says what actually happened.
125
- - **`vibo_healthcheck`** answers with a connectivity/auth diagnostic, for the
126
- same reason.
127
- - **The 13 remaining reads hand back Vibo's GraphQL payload as it arrived.**
128
- No rung is declared on them, so there is no slim option to ask for — and a
129
- `view` passed anyway is dropped by zod without a warning, so a successful
130
- call is not evidence the rung was honoured.
131
-
132
- That last group is not a neutral fact, so budget for it. **Five of those reads
133
- select media fields from Vibo and return them in full:**
134
-
135
- | Tool | What arrives per record |
136
- | --- | --- |
137
- | `vibo_get_section_songs` | `thumbnails { s180x180 original }` per song |
138
- | `vibo_list_song_ideas_songs` | `thumbnails { s180x180 original }` per song |
139
- | `vibo_list_event_users` | `imageUrl` per person |
140
- | `vibo_list_notifications` | `imageUrl` per notification |
141
- | `vibo_get_me` | `imageUrl` |
142
-
143
- `vibo_get_section_songs` is the one to watch: it returns the same catalog's
144
- song records that `vibo_search_songs` returns, carrying the same per-track
145
- cover art — but with no rung to drop it. Page it with `limit` rather than
146
- reaching for a `view` that is not there.
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.
147
142
 
148
143
  ## Typical flow
149
144