vibo-mcp 2.1.0 → 2.2.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": "2.1.0"
10
+ "version": "2.2.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": "2.1.0",
18
+ "version": "2.2.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": "2.1.0",
4
+ "version": "2.2.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
@@ -5,6 +5,31 @@ var __export = (target, all) => {
5
5
  __defProp(target, name, { get: all[name], enumerable: true });
6
6
  };
7
7
 
8
+ // node_modules/@chrischall/mcp-utils/dist/cancel/index.js
9
+ import { AsyncLocalStorage } from "node:async_hooks";
10
+ var storage = new AsyncLocalStorage();
11
+ function withCallSignal(signal, fn, request) {
12
+ return signal ? storage.run({ signal, ...request ? { request } : {} }, fn) : fn();
13
+ }
14
+ function currentCallSignal() {
15
+ return storage.getStore()?.signal;
16
+ }
17
+ function withAmbientCancellation(own2) {
18
+ const ambient = currentCallSignal();
19
+ if (!ambient)
20
+ return own2;
21
+ if (!own2)
22
+ return ambient;
23
+ return AbortSignal.any([own2, ambient]);
24
+ }
25
+
26
+ // node_modules/@chrischall/mcp-utils/dist/caller/index.js
27
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
28
+ var storage2 = new AsyncLocalStorage2();
29
+ function withCallerCapabilities(capabilities, fn) {
30
+ return capabilities ? storage2.run(capabilities, fn) : fn();
31
+ }
32
+
8
33
  // node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
9
34
  var __create = Object.create;
10
35
  var __defProp2 = Object.defineProperty;
@@ -34474,17 +34499,73 @@ Hint: ${err.hint}`);
34474
34499
  }
34475
34500
  throw err;
34476
34501
  }
34502
+ function mcpRequestFrom(args) {
34503
+ const ctx = args.at(-1);
34504
+ if (typeof ctx !== "object" || ctx === null)
34505
+ return void 0;
34506
+ const req = ctx.mcpReq;
34507
+ if (typeof req !== "object" || req === null)
34508
+ return void 0;
34509
+ return typeof req.notify === "function" ? req : void 0;
34510
+ }
34511
+ function callSignalFrom(args) {
34512
+ const ctx = args.at(-1);
34513
+ if (typeof ctx !== "object" || ctx === null)
34514
+ return void 0;
34515
+ const req = ctx.mcpReq;
34516
+ if (typeof req !== "object" || req === null)
34517
+ return void 0;
34518
+ const signal = req.signal;
34519
+ return signal instanceof AbortSignal ? signal : void 0;
34520
+ }
34521
+ function declaredCapabilitiesFrom(server) {
34522
+ const inner = server.server;
34523
+ if (typeof inner !== "object" || inner === null)
34524
+ return void 0;
34525
+ const read = inner.getClientCapabilities;
34526
+ if (typeof read !== "function")
34527
+ return void 0;
34528
+ try {
34529
+ const declared = read.call(inner);
34530
+ return typeof declared === "object" && declared !== null ? declared : void 0;
34531
+ } catch {
34532
+ return void 0;
34533
+ }
34534
+ }
34477
34535
  function surfaceToolHints(server) {
34478
34536
  const register2 = server.registerTool.bind(server);
34479
- server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => {
34480
- let result;
34481
- try {
34482
- result = cb(...args);
34483
- } catch (err) {
34484
- return hintResultOrRethrow(err);
34485
- }
34486
- return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
34487
- });
34537
+ server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => (
34538
+ // THE CALLER'S CANCELLATION, made ambient for the whole handler
34539
+ // (`cancel/index.ts`). The SDK delivers it and the fleet ignored it:
34540
+ // measured against @modelcontextprotocol/server 2.0.0, a cancelled
34541
+ // call aborts `ctx.mcpReq.signal` with the caller's reason and the
34542
+ // handler runs to completion regardless — so the HTTP request stays
34543
+ // in flight, the child keeps burning metered CPU, and the upstream
34544
+ // keeps being hit for somebody who has gone. claude.ai sent 101
34545
+ // cancellations in the week to 2026-09-20.
34546
+ //
34547
+ // Here because this is the one wrapper every tool already passes
34548
+ // through: threading the signal by hand would mean editing several
34549
+ // hundred handlers and missing exactly the ones nobody edits.
34550
+ withCallSignal(callSignalFrom(args), () => (
34551
+ // WHAT THE CALLER CAN DO, made ambient for the same reason and in
34552
+ // the same place (`caller/index.ts`). Only the 2025-era half needs
34553
+ // the wrapper — a 2026-07-28 request answers for itself off its own
34554
+ // envelope — but a guarded tool that cannot tell whether the caller
34555
+ // can be shown a prompt returns one the SDK then refuses to deliver,
34556
+ // and that refusal reaches the caller as an unexplained protocol
34557
+ // error it had no chance to catch.
34558
+ withCallerCapabilities(declaredCapabilitiesFrom(server), () => {
34559
+ let result;
34560
+ try {
34561
+ result = cb(...args);
34562
+ } catch (err) {
34563
+ return hintResultOrRethrow(err);
34564
+ }
34565
+ return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
34566
+ })
34567
+ ), mcpRequestFrom(args))
34568
+ ));
34488
34569
  }
34489
34570
  async function createMcpServer(opts) {
34490
34571
  const server = new McpServer({ name: opts.name, version: opts.version }, { supportedProtocolVersions: [...SERVER_PROTOCOL_VERSIONS] });
@@ -34597,12 +34678,13 @@ function toolAnnotations(opts = {}) {
34597
34678
  ...opts.title !== void 0 ? { title: opts.title } : {},
34598
34679
  readOnlyHint: opts.readOnly ?? true,
34599
34680
  ...opts.idempotent !== void 0 ? { idempotentHint: opts.idempotent } : {},
34681
+ ...opts.destructive !== void 0 ? { destructiveHint: opts.destructive } : {},
34600
34682
  ...opts.openWorld !== void 0 ? { openWorldHint: opts.openWorld } : {}
34601
34683
  };
34602
34684
  }
34603
34685
 
34604
34686
  // src/version.ts
34605
- var VERSION = "2.1.0";
34687
+ var VERSION = "2.2.1";
34606
34688
 
34607
34689
  // src/client.ts
34608
34690
  import { dirname as dirname2, join as join3 } from "path";
@@ -34807,6 +34889,9 @@ var REFRESH = `
34807
34889
  }
34808
34890
  `;
34809
34891
  var AUTH_ERROR_CODES = /* @__PURE__ */ new Set(["UNAUTHORIZED", "UNAUTHENTICATED", "FORBIDDEN"]);
34892
+ function requestSignal() {
34893
+ return withAmbientCancellation(AbortSignal.timeout(REQUEST_TIMEOUT_MS));
34894
+ }
34810
34895
  var ViboClient = class {
34811
34896
  apiUrl;
34812
34897
  email;
@@ -34931,7 +35016,7 @@ var ViboClient = class {
34931
35016
  headers,
34932
35017
  // NB: no content-type — fetch sets the multipart boundary
34933
35018
  body: form,
34934
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
35019
+ signal: requestSignal()
34935
35020
  });
34936
35021
  } catch (err) {
34937
35022
  const reason = err instanceof Error && err.name === "TimeoutError" ? "timed out" : "failed";
@@ -35021,7 +35106,7 @@ var ViboClient = class {
35021
35106
  method: "POST",
35022
35107
  headers,
35023
35108
  body: JSON.stringify({ query, variables }),
35024
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
35109
+ signal: requestSignal()
35025
35110
  });
35026
35111
  } catch (err) {
35027
35112
  const reason = err instanceof Error && err.name === "TimeoutError" ? "timed out" : "failed";
@@ -35478,7 +35563,7 @@ function registerEventTools(server, client2) {
35478
35563
  "vibo_join_event",
35479
35564
  {
35480
35565
  description: "Join an event you were invited to, via its share link or hash (e.g. a vibodj.app.link/... URL someone sent you). Returns the joined event's id. Confirm-gated.",
35481
- annotations: toolAnnotations({ title: "Join Vibo event", readOnly: false }),
35566
+ annotations: toolAnnotations({ title: "Join Vibo event", readOnly: false, destructive: false }),
35482
35567
  inputSchema: external_exports.object({
35483
35568
  link: external_exports.string().describe("The full event share URL (vibodj.app.link/... or web.vibodj.com/...) or the bare join hash."),
35484
35569
  confirm: schemaConfirm
@@ -35503,7 +35588,7 @@ function registerEventTools(server, client2) {
35503
35588
  "vibo_leave_event",
35504
35589
  {
35505
35590
  description: "Leave an event you previously joined. Confirm-gated.",
35506
- annotations: toolAnnotations({ title: "Leave Vibo event", readOnly: false }),
35591
+ annotations: toolAnnotations({ title: "Leave Vibo event", readOnly: false, destructive: true }),
35507
35592
  inputSchema: external_exports.object({
35508
35593
  eventId: external_exports.string().describe("Event id to leave."),
35509
35594
  confirm: schemaConfirm
@@ -35519,7 +35604,7 @@ function registerEventTools(server, client2) {
35519
35604
  "vibo_create_event_contact",
35520
35605
  {
35521
35606
  description: "Add a contact (host or guest) to an event with their name/email/phone. Confirm-gated.",
35522
- annotations: toolAnnotations({ title: "Add Vibo event contact", readOnly: false }),
35607
+ annotations: toolAnnotations({ title: "Add Vibo event contact", readOnly: false, destructive: false }),
35523
35608
  inputSchema: external_exports.object({
35524
35609
  eventId: external_exports.string().describe("Event id."),
35525
35610
  role: external_exports.enum(["host", "guest"]).describe("The contact's role in the event."),
@@ -35763,7 +35848,7 @@ function registerSongTools(server, client2) {
35763
35848
  "vibo_add_song_to_section",
35764
35849
  {
35765
35850
  description: "Add a song to a section. Pass a song from vibo_search_songs (songUrl is required; include viboSongId/title/artist when known). Before adding, check that result's `quality.confidence`: adding a `likely-not-original` result puts a cover, karaoke track or junk-metadata re-upload in front of a live DJ. If nothing looks original, report the closest matches back rather than adding a best guess. Confirm-gated.",
35766
- annotations: toolAnnotations({ title: "Add song to Vibo section", readOnly: false }),
35851
+ annotations: toolAnnotations({ title: "Add song to Vibo section", readOnly: false, destructive: false }),
35767
35852
  inputSchema: external_exports.object({
35768
35853
  eventId: external_exports.string().describe("Event id."),
35769
35854
  sectionId: external_exports.string().describe("Section id to add the song to."),
@@ -35793,7 +35878,7 @@ function registerSongTools(server, client2) {
35793
35878
  "vibo_toggle_song_like",
35794
35879
  {
35795
35880
  description: "Like or unlike a song in a section. Confirm-gated.",
35796
- annotations: toolAnnotations({ title: "Like/unlike Vibo song", readOnly: false }),
35881
+ annotations: toolAnnotations({ title: "Like/unlike Vibo song", readOnly: false, destructive: false }),
35797
35882
  inputSchema: external_exports.object({
35798
35883
  eventId: external_exports.string().describe("Event id."),
35799
35884
  sectionId: external_exports.string().describe("Section id."),
@@ -35864,7 +35949,7 @@ function registerPlaylistTools(server, client2) {
35864
35949
  "vibo_export_event_to_spotify",
35865
35950
  {
35866
35951
  description: "Export an event's song selections to a new Spotify playlist (Spotify must be connected). Returns the playlist URL plus how many tracks exported / failed. Confirm-gated.",
35867
- annotations: toolAnnotations({ title: "Export event to Spotify", readOnly: false }),
35952
+ annotations: toolAnnotations({ title: "Export event to Spotify", readOnly: false, destructive: false }),
35868
35953
  inputSchema: external_exports.object({
35869
35954
  eventId: external_exports.string().describe("Event id."),
35870
35955
  sectionIds: external_exports.array(external_exports.string()).min(1).describe("Section ids to include (from vibo_list_sections)."),
@@ -35886,7 +35971,7 @@ function registerPlaylistTools(server, client2) {
35886
35971
  "vibo_export_event_to_apple_music",
35887
35972
  {
35888
35973
  description: "Export an event's song selections to a new Apple Music playlist (Apple Music must be connected). Returns the playlist URL plus how many tracks exported / failed. Confirm-gated.",
35889
- annotations: toolAnnotations({ title: "Export event to Apple Music", readOnly: false }),
35974
+ annotations: toolAnnotations({ title: "Export event to Apple Music", readOnly: false, destructive: false }),
35890
35975
  inputSchema: external_exports.object({
35891
35976
  eventId: external_exports.string().describe("Event id."),
35892
35977
  sectionIds: external_exports.array(external_exports.string()).min(1).describe("Section ids to include (from vibo_list_sections)."),
@@ -35941,7 +36026,7 @@ function registerNotificationTools(server, client2) {
35941
36026
  "vibo_mark_notifications_read",
35942
36027
  {
35943
36028
  description: "Mark notifications as read \u2014 pass specific notificationIds, or readAll:true to clear everything. Confirm-gated.",
35944
- annotations: toolAnnotations({ title: "Mark notifications read", readOnly: false }),
36029
+ annotations: toolAnnotations({ title: "Mark notifications read", readOnly: false, destructive: false }),
35945
36030
  inputSchema: external_exports.object({
35946
36031
  notificationIds: external_exports.array(external_exports.string()).optional().describe("Specific notification ids to mark read."),
35947
36032
  readAll: external_exports.boolean().optional().describe("Mark every notification as read."),
@@ -36026,7 +36111,7 @@ function registerQuestionTools(server, client2, resolveUpload = nodeUploadResolv
36026
36111
  "vibo_answer_question",
36027
36112
  {
36028
36113
  description: "Answer a section planning question. Provide the field matching the question's type: `text` for a text question, `selectedOptions` (array of option _ids from vibo_list_section_questions) for radio/checkbox/select, or `link` (array of URLs) for a link question. Use `otherOptionTitle` with the question's \"other\" option. For photo/file questions, pass local paths (`imagePaths`/`filePaths`) when the server can read your disk, or inline base64 bytes (`images`/`files`) otherwise. Confirm-gated.",
36029
- annotations: toolAnnotations({ title: "Answer Vibo question", readOnly: false }),
36114
+ annotations: toolAnnotations({ title: "Answer Vibo question", readOnly: false, destructive: false }),
36030
36115
  inputSchema: external_exports.object({
36031
36116
  eventId: external_exports.string().describe("Event id."),
36032
36117
  sectionId: external_exports.string().describe("Section id."),
@@ -36110,7 +36195,7 @@ function registerSongManagementTools(server, client2) {
36110
36195
  "vibo_remove_song_from_section",
36111
36196
  {
36112
36197
  description: "Remove one or more songs from a section. Confirm-gated.",
36113
- annotations: toolAnnotations({ title: "Remove songs from Vibo section", readOnly: false }),
36198
+ annotations: toolAnnotations({ title: "Remove songs from Vibo section", readOnly: false, destructive: true }),
36114
36199
  inputSchema: external_exports.object({
36115
36200
  eventId: external_exports.string().describe("Event id."),
36116
36201
  sectionId: external_exports.string().describe("Section id."),
@@ -36129,7 +36214,7 @@ function registerSongManagementTools(server, client2) {
36129
36214
  "vibo_update_song",
36130
36215
  {
36131
36216
  description: "Update songs in a section: mark must-play, flag as do-not-play, and/or set a comment. Provide at least one field. Confirm-gated.",
36132
- annotations: toolAnnotations({ title: "Update Vibo section songs", readOnly: false }),
36217
+ annotations: toolAnnotations({ title: "Update Vibo section songs", readOnly: false, destructive: false }),
36133
36218
  inputSchema: external_exports.object({
36134
36219
  eventId: external_exports.string().describe("Event id."),
36135
36220
  sectionId: external_exports.string().describe("Section id."),
@@ -36160,7 +36245,7 @@ function registerSongManagementTools(server, client2) {
36160
36245
  "vibo_move_song",
36161
36246
  {
36162
36247
  description: "Move songs from one section to another. Confirm-gated.",
36163
- annotations: toolAnnotations({ title: "Move Vibo section songs", readOnly: false }),
36248
+ annotations: toolAnnotations({ title: "Move Vibo section songs", readOnly: false, destructive: false }),
36164
36249
  inputSchema: external_exports.object({
36165
36250
  eventId: external_exports.string().describe("Event id."),
36166
36251
  sourceSectionId: external_exports.string().describe("Section id the songs are currently in."),
@@ -36180,7 +36265,7 @@ function registerSongManagementTools(server, client2) {
36180
36265
  "vibo_reorder_songs",
36181
36266
  {
36182
36267
  description: "Reorder songs within a section. Confirm-gated.",
36183
- annotations: toolAnnotations({ title: "Reorder Vibo section songs", readOnly: false }),
36268
+ annotations: toolAnnotations({ title: "Reorder Vibo section songs", readOnly: false, destructive: false }),
36184
36269
  inputSchema: external_exports.object({
36185
36270
  eventId: external_exports.string().describe("Event id."),
36186
36271
  sectionId: external_exports.string().describe("Section id."),
@@ -36204,7 +36289,7 @@ function registerCommentTools(server, client2) {
36204
36289
  "vibo_comment_on_song",
36205
36290
  {
36206
36291
  description: "Leave a comment / note for the DJ on a specific song. Confirm-gated.",
36207
- annotations: toolAnnotations({ title: "Comment on Vibo song", readOnly: false }),
36292
+ annotations: toolAnnotations({ title: "Comment on Vibo song", readOnly: false, destructive: false }),
36208
36293
  inputSchema: external_exports.object({
36209
36294
  eventId: external_exports.string().describe("Event id."),
36210
36295
  sectionId: external_exports.string().describe("Section id."),
@@ -36224,7 +36309,7 @@ function registerCommentTools(server, client2) {
36224
36309
  "vibo_delete_song_comment",
36225
36310
  {
36226
36311
  description: "Delete a comment on a song. Confirm-gated.",
36227
- annotations: toolAnnotations({ title: "Delete Vibo song comment", readOnly: false }),
36312
+ annotations: toolAnnotations({ title: "Delete Vibo song comment", readOnly: false, destructive: true }),
36228
36313
  inputSchema: external_exports.object({
36229
36314
  eventId: external_exports.string().describe("Event id."),
36230
36315
  sectionId: external_exports.string().describe("Section id."),
@@ -36244,7 +36329,7 @@ function registerCommentTools(server, client2) {
36244
36329
  "vibo_comment_on_section",
36245
36330
  {
36246
36331
  description: "Leave a comment on a timeline section. Confirm-gated.",
36247
- annotations: toolAnnotations({ title: "Comment on Vibo section", readOnly: false }),
36332
+ annotations: toolAnnotations({ title: "Comment on Vibo section", readOnly: false, destructive: false }),
36248
36333
  inputSchema: external_exports.object({
36249
36334
  eventId: external_exports.string().describe("Event id."),
36250
36335
  sectionId: external_exports.string().describe("Section id (from vibo_list_sections)."),
@@ -36263,7 +36348,7 @@ function registerCommentTools(server, client2) {
36263
36348
  "vibo_delete_section_comment",
36264
36349
  {
36265
36350
  description: "Delete a comment on a timeline section. Confirm-gated.",
36266
- annotations: toolAnnotations({ title: "Delete Vibo section comment", readOnly: false }),
36351
+ annotations: toolAnnotations({ title: "Delete Vibo section comment", readOnly: false, destructive: true }),
36267
36352
  inputSchema: external_exports.object({
36268
36353
  eventId: external_exports.string().describe("Event id."),
36269
36354
  sectionId: external_exports.string().describe("Section id (from vibo_list_sections)."),
@@ -36335,7 +36420,7 @@ function registerImportTools(server, client2) {
36335
36420
  "vibo_import_playlist_to_section",
36336
36421
  {
36337
36422
  description: "Import selected tracks from a connected Spotify/Apple Music playlist into a section. Returns counts of added/existing/ignored. Confirm-gated.",
36338
- annotations: toolAnnotations({ title: "Import playlist to section", readOnly: false }),
36423
+ annotations: toolAnnotations({ title: "Import playlist to section", readOnly: false, destructive: false }),
36339
36424
  inputSchema: external_exports.object({
36340
36425
  eventId: external_exports.string().describe("Event id."),
36341
36426
  sectionId: external_exports.string().describe("Section id (from vibo_list_sections)."),
@@ -36406,7 +36491,7 @@ function registerCollaborationTools(server, client2) {
36406
36491
  "vibo_invite_users",
36407
36492
  {
36408
36493
  description: "Invite people to an event by email (as host or guest). Confirm-gated.",
36409
- annotations: toolAnnotations({ title: "Invite Vibo event users", readOnly: false }),
36494
+ annotations: toolAnnotations({ title: "Invite Vibo event users", readOnly: false, destructive: true }),
36410
36495
  inputSchema: external_exports.object({
36411
36496
  eventId: external_exports.string().describe("Event id."),
36412
36497
  type: external_exports.enum(["host", "guest"]).describe("Invite as host or guest."),
@@ -36426,7 +36511,7 @@ function registerCollaborationTools(server, client2) {
36426
36511
  "vibo_change_user_role",
36427
36512
  {
36428
36513
  description: "Change an event member's role between host and guest. Confirm-gated.",
36429
- annotations: toolAnnotations({ title: "Change Vibo user role", readOnly: false }),
36514
+ annotations: toolAnnotations({ title: "Change Vibo user role", readOnly: false, destructive: true }),
36430
36515
  inputSchema: external_exports.object({
36431
36516
  eventId: external_exports.string().describe("Event id."),
36432
36517
  userId: external_exports.string().describe("Id of the member to update."),
@@ -36445,7 +36530,7 @@ function registerCollaborationTools(server, client2) {
36445
36530
  "vibo_remove_user",
36446
36531
  {
36447
36532
  description: "Remove a member from an event. Confirm-gated.",
36448
- annotations: toolAnnotations({ title: "Remove Vibo event user", readOnly: false }),
36533
+ annotations: toolAnnotations({ title: "Remove Vibo event user", readOnly: false, destructive: true }),
36449
36534
  inputSchema: external_exports.object({
36450
36535
  eventId: external_exports.string().describe("Event id."),
36451
36536
  userId: external_exports.string().describe("Id of the member to remove."),
@@ -36467,7 +36552,7 @@ function registerSectionEditTools(server, client2) {
36467
36552
  "vibo_update_section",
36468
36553
  {
36469
36554
  description: "Edit a timeline section's name, time, note, or description. Subject to the section's host-edit permissions. Confirm-gated.",
36470
- annotations: toolAnnotations({ title: "Update Vibo section", readOnly: false }),
36555
+ annotations: toolAnnotations({ title: "Update Vibo section", readOnly: false, destructive: false }),
36471
36556
  inputSchema: external_exports.object({
36472
36557
  eventId: external_exports.string(),
36473
36558
  sectionId: external_exports.string(),
@@ -36503,7 +36588,7 @@ function registerUploadTools(server, client2, resolveUpload = nodeUploadResolver
36503
36588
  "vibo_set_profile_photo",
36504
36589
  {
36505
36590
  description: "Set your Vibo profile photo from an image. Pass a local file `path` if the server shares your filesystem; otherwise pass the image bytes as base64 `fileData`. Returns the uploaded image URL. Confirm-gated.",
36506
- annotations: toolAnnotations({ title: "Set Vibo profile photo", readOnly: false }),
36591
+ annotations: toolAnnotations({ title: "Set Vibo profile photo", readOnly: false, destructive: false }),
36507
36592
  inputSchema: external_exports.object({
36508
36593
  path: external_exports.string().optional().describe("Absolute path to a local image file (jpg/png). Local/stdio server only."),
36509
36594
  fileData: external_exports.string().optional().describe("Base64-encoded image bytes (a `data:` URL prefix is allowed). Use this when the server cannot read your filesystem."),
@@ -36581,7 +36666,7 @@ function registerSessionTools(server, client2) {
36581
36666
  "vibo_capture_session",
36582
36667
  {
36583
36668
  description: "Capture your Vibo login from a signed-in web.vibodj.com browser tab via the fetchproxy bridge \u2014 for accounts that sign in with Apple/Google/Facebook (no password). Requires the fetchproxy browser extension installed and you signed into https://web.vibodj.com; approve the pair code shown on first use. The token is saved locally and reused on future calls.",
36584
- annotations: toolAnnotations({ title: "Capture Vibo session (SSO)", readOnly: false })
36669
+ annotations: toolAnnotations({ title: "Capture Vibo session (SSO)", readOnly: false, destructive: false })
36585
36670
  },
36586
36671
  async () => {
36587
36672
  const { accessToken, refreshToken } = await captureViboSession();
package/dist/client.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { dirname, join } from 'path';
2
2
  import { fileURLToPath } from 'url';
3
- import { loadDotenvSafely, readEnvVar, McpToolError, SessionNotAuthenticatedError, truncateErrorMessage, } from '@chrischall/mcp-utils';
3
+ import { loadDotenvSafely, readEnvVar, McpToolError, SessionNotAuthenticatedError, truncateErrorMessage, withAmbientCancellation, } from '@chrischall/mcp-utils';
4
4
  import { loadSession, saveSession } from './session-store.js';
5
5
  // Load .env for local dev; silently skip if dotenv is unavailable (e.g. the
6
6
  // mcpb bundle, which externalizes dotenv). `override: false` means a
@@ -40,6 +40,25 @@ const REFRESH = `
40
40
  // Error codes Vibo (and conventional GraphQL servers) use for an expired /
41
41
  // missing session — these should trigger a token refresh + replay.
42
42
  const AUTH_ERROR_CODES = new Set(['UNAUTHORIZED', 'UNAUTHENTICATED', 'FORBIDDEN']);
43
+ /**
44
+ * The signal every Vibo request is made with: our timeout, and the caller's
45
+ * cancellation if there is one (mcp-utils `cancel`).
46
+ *
47
+ * Until this, only the timeout could stop a request — a cancelled tool call
48
+ * held it open for the full budget while the child burned the CPU mcp-host
49
+ * meters it on. Measured on that fleet: claude.ai sent 101 cancellations in
50
+ * the week to 2026-09-20.
51
+ *
52
+ * ONE definition for both request paths, which is not tidiness: the two are
53
+ * the multipart upload and the plain query, they had the same seven lines
54
+ * copied between them, and the next person to add a third path is the one
55
+ * this saves. The `TimeoutError` checks at both call sites still name a real
56
+ * timeout — an abort from the caller arrives as `AbortError` and falls
57
+ * through to 'failed'.
58
+ */
59
+ function requestSignal() {
60
+ return withAmbientCancellation(AbortSignal.timeout(REQUEST_TIMEOUT_MS));
61
+ }
43
62
  export class ViboClient {
44
63
  apiUrl;
45
64
  email;
@@ -172,7 +191,7 @@ export class ViboClient {
172
191
  method: 'POST',
173
192
  headers, // NB: no content-type — fetch sets the multipart boundary
174
193
  body: form,
175
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
194
+ signal: requestSignal(),
176
195
  });
177
196
  }
178
197
  catch (err) {
@@ -265,7 +284,7 @@ export class ViboClient {
265
284
  method: 'POST',
266
285
  headers,
267
286
  body: JSON.stringify({ query, variables }),
268
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
287
+ signal: requestSignal(),
269
288
  });
270
289
  }
271
290
  catch (err) {
@@ -42,7 +42,7 @@ export function registerCollaborationTools(server, client) {
42
42
  });
43
43
  server.registerTool('vibo_invite_users', {
44
44
  description: 'Invite people to an event by email (as host or guest). Confirm-gated.',
45
- annotations: toolAnnotations({ title: 'Invite Vibo event users', readOnly: false }),
45
+ annotations: toolAnnotations({ title: 'Invite Vibo event users', readOnly: false, destructive: true }),
46
46
  inputSchema: z.object({
47
47
  eventId: z.string().describe('Event id.'),
48
48
  type: z.enum(['host', 'guest']).describe('Invite as host or guest.'),
@@ -59,7 +59,7 @@ export function registerCollaborationTools(server, client) {
59
59
  });
60
60
  server.registerTool('vibo_change_user_role', {
61
61
  description: "Change an event member's role between host and guest. Confirm-gated.",
62
- annotations: toolAnnotations({ title: 'Change Vibo user role', readOnly: false }),
62
+ annotations: toolAnnotations({ title: 'Change Vibo user role', readOnly: false, destructive: true }),
63
63
  inputSchema: z.object({
64
64
  eventId: z.string().describe('Event id.'),
65
65
  userId: z.string().describe('Id of the member to update.'),
@@ -75,7 +75,7 @@ export function registerCollaborationTools(server, client) {
75
75
  });
76
76
  server.registerTool('vibo_remove_user', {
77
77
  description: 'Remove a member from an event. Confirm-gated.',
78
- annotations: toolAnnotations({ title: 'Remove Vibo event user', readOnly: false }),
78
+ annotations: toolAnnotations({ title: 'Remove Vibo event user', readOnly: false, destructive: true }),
79
79
  inputSchema: z.object({
80
80
  eventId: z.string().describe('Event id.'),
81
81
  userId: z.string().describe('Id of the member to remove.'),
@@ -5,7 +5,7 @@ import { previewResult } from './shared.js';
5
5
  export function registerCommentTools(server, client) {
6
6
  server.registerTool('vibo_comment_on_song', {
7
7
  description: 'Leave a comment / note for the DJ on a specific song. Confirm-gated.',
8
- annotations: toolAnnotations({ title: 'Comment on Vibo song', readOnly: false }),
8
+ annotations: toolAnnotations({ title: 'Comment on Vibo song', readOnly: false, destructive: false }),
9
9
  inputSchema: z.object({
10
10
  eventId: z.string().describe('Event id.'),
11
11
  sectionId: z.string().describe('Section id.'),
@@ -22,7 +22,7 @@ export function registerCommentTools(server, client) {
22
22
  });
23
23
  server.registerTool('vibo_delete_song_comment', {
24
24
  description: 'Delete a comment on a song. Confirm-gated.',
25
- annotations: toolAnnotations({ title: 'Delete Vibo song comment', readOnly: false }),
25
+ annotations: toolAnnotations({ title: 'Delete Vibo song comment', readOnly: false, destructive: true }),
26
26
  inputSchema: z.object({
27
27
  eventId: z.string().describe('Event id.'),
28
28
  sectionId: z.string().describe('Section id.'),
@@ -39,7 +39,7 @@ export function registerCommentTools(server, client) {
39
39
  });
40
40
  server.registerTool('vibo_comment_on_section', {
41
41
  description: 'Leave a comment on a timeline section. Confirm-gated.',
42
- annotations: toolAnnotations({ title: 'Comment on Vibo section', readOnly: false }),
42
+ annotations: toolAnnotations({ title: 'Comment on Vibo section', readOnly: false, destructive: false }),
43
43
  inputSchema: z.object({
44
44
  eventId: z.string().describe('Event id.'),
45
45
  sectionId: z.string().describe('Section id (from vibo_list_sections).'),
@@ -55,7 +55,7 @@ export function registerCommentTools(server, client) {
55
55
  });
56
56
  server.registerTool('vibo_delete_section_comment', {
57
57
  description: 'Delete a comment on a timeline section. Confirm-gated.',
58
- annotations: toolAnnotations({ title: 'Delete Vibo section comment', readOnly: false }),
58
+ annotations: toolAnnotations({ title: 'Delete Vibo section comment', readOnly: false, destructive: true }),
59
59
  inputSchema: z.object({
60
60
  eventId: z.string().describe('Event id.'),
61
61
  sectionId: z.string().describe('Section id (from vibo_list_sections).'),
@@ -33,7 +33,7 @@ export function registerEventTools(server, client) {
33
33
  });
34
34
  server.registerTool('vibo_join_event', {
35
35
  description: "Join an event you were invited to, via its share link or hash (e.g. a vibodj.app.link/... URL someone sent you). Returns the joined event's id. Confirm-gated.",
36
- annotations: toolAnnotations({ title: 'Join Vibo event', readOnly: false }),
36
+ annotations: toolAnnotations({ title: 'Join Vibo event', readOnly: false, destructive: false }),
37
37
  inputSchema: z.object({
38
38
  link: z
39
39
  .string()
@@ -56,7 +56,7 @@ export function registerEventTools(server, client) {
56
56
  });
57
57
  server.registerTool('vibo_leave_event', {
58
58
  description: 'Leave an event you previously joined. Confirm-gated.',
59
- annotations: toolAnnotations({ title: 'Leave Vibo event', readOnly: false }),
59
+ annotations: toolAnnotations({ title: 'Leave Vibo event', readOnly: false, destructive: true }),
60
60
  inputSchema: z.object({
61
61
  eventId: z.string().describe('Event id to leave.'),
62
62
  confirm: schemaConfirm,
@@ -69,7 +69,7 @@ export function registerEventTools(server, client) {
69
69
  });
70
70
  server.registerTool('vibo_create_event_contact', {
71
71
  description: 'Add a contact (host or guest) to an event with their name/email/phone. Confirm-gated.',
72
- annotations: toolAnnotations({ title: 'Add Vibo event contact', readOnly: false }),
72
+ annotations: toolAnnotations({ title: 'Add Vibo event contact', readOnly: false, destructive: false }),
73
73
  inputSchema: z.object({
74
74
  eventId: z.string().describe('Event id.'),
75
75
  role: z.enum(['host', 'guest']).describe("The contact's role in the event."),
@@ -5,7 +5,7 @@ import { previewResult } from './shared.js';
5
5
  export function registerImportTools(server, client) {
6
6
  server.registerTool('vibo_import_playlist_to_section', {
7
7
  description: 'Import selected tracks from a connected Spotify/Apple Music playlist into a section. Returns counts of added/existing/ignored. Confirm-gated.',
8
- annotations: toolAnnotations({ title: 'Import playlist to section', readOnly: false }),
8
+ annotations: toolAnnotations({ title: 'Import playlist to section', readOnly: false, destructive: false }),
9
9
  inputSchema: z.object({
10
10
  eventId: z.string().describe('Event id.'),
11
11
  sectionId: z.string().describe('Section id (from vibo_list_sections).'),
@@ -27,7 +27,7 @@ export function registerNotificationTools(server, client) {
27
27
  });
28
28
  server.registerTool('vibo_mark_notifications_read', {
29
29
  description: 'Mark notifications as read — pass specific notificationIds, or readAll:true to clear everything. Confirm-gated.',
30
- annotations: toolAnnotations({ title: 'Mark notifications read', readOnly: false }),
30
+ annotations: toolAnnotations({ title: 'Mark notifications read', readOnly: false, destructive: false }),
31
31
  inputSchema: z.object({
32
32
  notificationIds: z.array(z.string()).optional().describe('Specific notification ids to mark read.'),
33
33
  readAll: z.boolean().optional().describe('Mark every notification as read.'),
@@ -42,7 +42,7 @@ export function registerPlaylistTools(server, client) {
42
42
  });
43
43
  server.registerTool('vibo_export_event_to_spotify', {
44
44
  description: "Export an event's song selections to a new Spotify playlist (Spotify must be connected). Returns the playlist URL plus how many tracks exported / failed. Confirm-gated.",
45
- annotations: toolAnnotations({ title: 'Export event to Spotify', readOnly: false }),
45
+ annotations: toolAnnotations({ title: 'Export event to Spotify', readOnly: false, destructive: false }),
46
46
  inputSchema: z.object({
47
47
  eventId: z.string().describe('Event id.'),
48
48
  sectionIds: z.array(z.string()).min(1).describe('Section ids to include (from vibo_list_sections).'),
@@ -63,7 +63,7 @@ export function registerPlaylistTools(server, client) {
63
63
  });
64
64
  server.registerTool('vibo_export_event_to_apple_music', {
65
65
  description: "Export an event's song selections to a new Apple Music playlist (Apple Music must be connected). Returns the playlist URL plus how many tracks exported / failed. Confirm-gated.",
66
- annotations: toolAnnotations({ title: 'Export event to Apple Music', readOnly: false }),
66
+ annotations: toolAnnotations({ title: 'Export event to Apple Music', readOnly: false, destructive: false }),
67
67
  inputSchema: z.object({
68
68
  eventId: z.string().describe('Event id.'),
69
69
  sectionIds: z.array(z.string()).min(1).describe('Section ids to include (from vibo_list_sections).'),
@@ -25,7 +25,7 @@ export function registerQuestionTools(server, client, resolveUpload = nodeUpload
25
25
  });
26
26
  server.registerTool('vibo_answer_question', {
27
27
  description: "Answer a section planning question. Provide the field matching the question's type: `text` for a text question, `selectedOptions` (array of option _ids from vibo_list_section_questions) for radio/checkbox/select, or `link` (array of URLs) for a link question. Use `otherOptionTitle` with the question's \"other\" option. For photo/file questions, pass local paths (`imagePaths`/`filePaths`) when the server can read your disk, or inline base64 bytes (`images`/`files`) otherwise. Confirm-gated.",
28
- annotations: toolAnnotations({ title: 'Answer Vibo question', readOnly: false }),
28
+ annotations: toolAnnotations({ title: 'Answer Vibo question', readOnly: false, destructive: false }),
29
29
  inputSchema: z.object({
30
30
  eventId: z.string().describe('Event id.'),
31
31
  sectionId: z.string().describe('Section id.'),
@@ -5,7 +5,7 @@ import { previewResult } from './shared.js';
5
5
  export function registerSectionEditTools(server, client) {
6
6
  server.registerTool('vibo_update_section', {
7
7
  description: "Edit a timeline section's name, time, note, or description. Subject to the section's host-edit permissions. Confirm-gated.",
8
- annotations: toolAnnotations({ title: 'Update Vibo section', readOnly: false }),
8
+ annotations: toolAnnotations({ title: 'Update Vibo section', readOnly: false, destructive: false }),
9
9
  inputSchema: z.object({
10
10
  eventId: z.string(),
11
11
  sectionId: z.string(),
@@ -8,7 +8,7 @@ import { GET_ME } from '../gql.js';
8
8
  export function registerSessionTools(server, client) {
9
9
  server.registerTool('vibo_capture_session', {
10
10
  description: "Capture your Vibo login from a signed-in web.vibodj.com browser tab via the fetchproxy bridge — for accounts that sign in with Apple/Google/Facebook (no password). Requires the fetchproxy browser extension installed and you signed into https://web.vibodj.com; approve the pair code shown on first use. The token is saved locally and reused on future calls.",
11
- annotations: toolAnnotations({ title: 'Capture Vibo session (SSO)', readOnly: false }),
11
+ annotations: toolAnnotations({ title: 'Capture Vibo session (SSO)', readOnly: false, destructive: false }),
12
12
  }, async () => {
13
13
  const { accessToken, refreshToken } = await captureViboSession();
14
14
  client.setTokens(accessToken, refreshToken);
@@ -5,7 +5,7 @@ import { previewResult } from './shared.js';
5
5
  export function registerSongManagementTools(server, client) {
6
6
  server.registerTool('vibo_remove_song_from_section', {
7
7
  description: 'Remove one or more songs from a section. Confirm-gated.',
8
- annotations: toolAnnotations({ title: 'Remove songs from Vibo section', readOnly: false }),
8
+ annotations: toolAnnotations({ title: 'Remove songs from Vibo section', readOnly: false, destructive: true }),
9
9
  inputSchema: z.object({
10
10
  eventId: z.string().describe('Event id.'),
11
11
  sectionId: z.string().describe('Section id.'),
@@ -24,7 +24,7 @@ export function registerSongManagementTools(server, client) {
24
24
  });
25
25
  server.registerTool('vibo_update_song', {
26
26
  description: 'Update songs in a section: mark must-play, flag as do-not-play, and/or set a comment. Provide at least one field. Confirm-gated.',
27
- annotations: toolAnnotations({ title: 'Update Vibo section songs', readOnly: false }),
27
+ annotations: toolAnnotations({ title: 'Update Vibo section songs', readOnly: false, destructive: false }),
28
28
  inputSchema: z.object({
29
29
  eventId: z.string().describe('Event id.'),
30
30
  sectionId: z.string().describe('Section id.'),
@@ -58,7 +58,7 @@ export function registerSongManagementTools(server, client) {
58
58
  });
59
59
  server.registerTool('vibo_move_song', {
60
60
  description: 'Move songs from one section to another. Confirm-gated.',
61
- annotations: toolAnnotations({ title: 'Move Vibo section songs', readOnly: false }),
61
+ annotations: toolAnnotations({ title: 'Move Vibo section songs', readOnly: false, destructive: false }),
62
62
  inputSchema: z.object({
63
63
  eventId: z.string().describe('Event id.'),
64
64
  sourceSectionId: z.string().describe('Section id the songs are currently in.'),
@@ -78,7 +78,7 @@ export function registerSongManagementTools(server, client) {
78
78
  });
79
79
  server.registerTool('vibo_reorder_songs', {
80
80
  description: 'Reorder songs within a section. Confirm-gated.',
81
- annotations: toolAnnotations({ title: 'Reorder Vibo section songs', readOnly: false }),
81
+ annotations: toolAnnotations({ title: 'Reorder Vibo section songs', readOnly: false, destructive: false }),
82
82
  inputSchema: z.object({
83
83
  eventId: z.string().describe('Event id.'),
84
84
  sectionId: z.string().describe('Section id.'),
@@ -96,7 +96,7 @@ export function registerSongTools(server, client) {
96
96
  '`quality.confidence`: adding a `likely-not-original` result puts a cover, karaoke ' +
97
97
  'track or junk-metadata re-upload in front of a live DJ. If nothing looks original, ' +
98
98
  'report the closest matches back rather than adding a best guess. Confirm-gated.',
99
- annotations: toolAnnotations({ title: 'Add song to Vibo section', readOnly: false }),
99
+ annotations: toolAnnotations({ title: 'Add song to Vibo section', readOnly: false, destructive: false }),
100
100
  inputSchema: z.object({
101
101
  eventId: z.string().describe('Event id.'),
102
102
  sectionId: z.string().describe('Section id to add the song to.'),
@@ -126,7 +126,7 @@ export function registerSongTools(server, client) {
126
126
  });
127
127
  server.registerTool('vibo_toggle_song_like', {
128
128
  description: 'Like or unlike a song in a section. Confirm-gated.',
129
- annotations: toolAnnotations({ title: 'Like/unlike Vibo song', readOnly: false }),
129
+ annotations: toolAnnotations({ title: 'Like/unlike Vibo song', readOnly: false, destructive: false }),
130
130
  inputSchema: z.object({
131
131
  eventId: z.string().describe('Event id.'),
132
132
  sectionId: z.string().describe('Section id.'),
@@ -12,7 +12,7 @@ import { previewResult } from './shared.js';
12
12
  export function registerUploadTools(server, client, resolveUpload = nodeUploadResolver) {
13
13
  server.registerTool('vibo_set_profile_photo', {
14
14
  description: 'Set your Vibo profile photo from an image. Pass a local file `path` if the server shares your filesystem; otherwise pass the image bytes as base64 `fileData`. Returns the uploaded image URL. Confirm-gated.',
15
- annotations: toolAnnotations({ title: 'Set Vibo profile photo', readOnly: false }),
15
+ annotations: toolAnnotations({ title: 'Set Vibo profile photo', readOnly: false, destructive: false }),
16
16
  inputSchema: z.object({
17
17
  path: z.string().optional().describe('Absolute path to a local image file (jpg/png). Local/stdio server only.'),
18
18
  fileData: z
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.1.0'; // x-release-please-version
5
+ export const VERSION = '2.2.1'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibo-mcp",
3
- "version": "2.1.0",
3
+ "version": "2.2.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,7 +46,7 @@
46
46
  "typecheck": "tsc -p tsconfig.json --noEmit"
47
47
  },
48
48
  "dependencies": {
49
- "@chrischall/mcp-utils": "^1.0.0",
49
+ "@chrischall/mcp-utils": "^2.2.0",
50
50
  "@fetchproxy/bootstrap": "^3.0.1",
51
51
  "@modelcontextprotocol/server": "^2.0.0",
52
52
  "dotenv": "^17.4.0",
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.1.0",
9
+ "version": "2.2.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "vibo-mcp",
14
- "version": "2.1.0",
14
+ "version": "2.2.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },