vibo-mcp 1.3.0 → 1.3.2

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.3.0"
10
+ "version": "1.3.2"
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.3.0",
18
+ "version": "1.3.2",
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.3.0",
4
+ "version": "1.3.2",
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",
@@ -18,6 +18,6 @@
18
18
  "music",
19
19
  "mcp"
20
20
  ],
21
- "skills": "./SKILL.md",
21
+ "skills": "./skills/",
22
22
  "mcp": "./.mcp.json"
23
23
  }
package/README.md CHANGED
@@ -48,7 +48,7 @@ confirm-gated: without `confirm: true` it returns a dry-run preview and makes no
48
48
  network call.
49
49
 
50
50
  See [docs/VIBO-API.md](docs/VIBO-API.md) for the reverse-engineered API notes
51
- and [SKILL.md](SKILL.md) for the full tool list.
51
+ and [skills/vibo-mcp/SKILL.md](skills/vibo-mcp/SKILL.md) for the full tool list.
52
52
 
53
53
  ## Development
54
54
 
package/dist/bundle.js CHANGED
@@ -31040,8 +31040,12 @@ var API_KEY_RE = new RegExp([
31040
31040
  // webhook signing secret (Stripe-style)
31041
31041
  ].map((p) => `\\b${p}`).join("|"), "g");
31042
31042
  var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
31043
+ var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
31044
+ var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
31045
+ var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
31046
+ var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
31043
31047
  function redactSecrets(text) {
31044
- return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
31048
+ return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
31045
31049
  }
31046
31050
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
31047
31051
  const str = text === null || text === void 0 ? "" : String(text);
@@ -31116,7 +31120,7 @@ function toolAnnotations(opts = {}) {
31116
31120
  }
31117
31121
 
31118
31122
  // src/version.ts
31119
- var VERSION = "1.3.0";
31123
+ var VERSION = "1.3.2";
31120
31124
 
31121
31125
  // src/client.ts
31122
31126
  import { dirname as dirname2, join as join2 } from "path";
@@ -31124,18 +31128,162 @@ import { fileURLToPath } from "url";
31124
31128
 
31125
31129
  // src/session-store.ts
31126
31130
  import { homedir } from "os";
31127
- import { dirname, join } from "path";
31128
- import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync, chmodSync } from "fs";
31129
- function sessionFile() {
31130
- return readEnvVar("VIBO_SESSION_FILE") ?? join(homedir(), ".vibo-mcp", "session.json");
31131
+ import { join } from "path";
31132
+
31133
+ // node_modules/@chrischall/mcp-utils/dist/session/index.js
31134
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync } from "node:fs";
31135
+ import { dirname } from "node:path";
31136
+ function normalizeOrigin(input) {
31137
+ try {
31138
+ return new URL(input).origin.replace(/\/$/, "");
31139
+ } catch {
31140
+ return input.replace(/\/$/, "");
31141
+ }
31142
+ }
31143
+ var SessionStore = class {
31144
+ sessions = /* @__PURE__ */ new Map();
31145
+ mostRecentKey = null;
31146
+ filePath;
31147
+ keyOf;
31148
+ normalizeKey;
31149
+ constructor(opts) {
31150
+ this.filePath = opts.filePath;
31151
+ this.keyOf = opts.keyOf;
31152
+ this.normalizeKey = opts.normalizeKey ?? normalizeOrigin;
31153
+ this.loadFromDisk();
31154
+ }
31155
+ loadFromDisk() {
31156
+ if (!existsSync(this.filePath))
31157
+ return;
31158
+ try {
31159
+ this.sessions = this.deserialize(readFileSync(this.filePath, "utf8"));
31160
+ const keys = Array.from(this.sessions.keys());
31161
+ this.mostRecentKey = keys[keys.length - 1] ?? null;
31162
+ } catch (err) {
31163
+ const backupPath = this.preserveCorruptFile();
31164
+ const detail = err instanceof Error ? err.message : String(err);
31165
+ console.error(`[mcp-utils] SessionStore: failed to parse ${this.filePath} (${detail}); ` + (backupPath !== null ? `preserved the corrupt file at ${backupPath}; ` : "could not preserve the corrupt file; ") + "starting with an empty store.");
31166
+ this.sessions = /* @__PURE__ */ new Map();
31167
+ this.mostRecentKey = null;
31168
+ }
31169
+ }
31170
+ /**
31171
+ * Move a corrupt store file to `<filePath>.corrupt` (or `.corrupt-<n>` if a
31172
+ * previous backup exists) so a subsequent save cannot overwrite the only
31173
+ * copy of the prior credentials. Best-effort: returns the backup path, or
31174
+ * `null` if the rename failed.
31175
+ */
31176
+ preserveCorruptFile() {
31177
+ try {
31178
+ let candidate = `${this.filePath}.corrupt`;
31179
+ for (let n = 1; existsSync(candidate) && n <= 100; n++) {
31180
+ candidate = `${this.filePath}.corrupt-${n}`;
31181
+ }
31182
+ if (existsSync(candidate))
31183
+ return null;
31184
+ renameSync(this.filePath, candidate);
31185
+ return candidate;
31186
+ } catch {
31187
+ return null;
31188
+ }
31189
+ }
31190
+ /** Serialize the store to its on-disk JSON form (array, insertion order). */
31191
+ serialize() {
31192
+ return JSON.stringify(Array.from(this.sessions.values()), null, 2);
31193
+ }
31194
+ /** Parse on-disk JSON back into a keyed `Map`; empty map on invalid input. */
31195
+ deserialize(body) {
31196
+ const map2 = /* @__PURE__ */ new Map();
31197
+ const arr = JSON.parse(body);
31198
+ if (!Array.isArray(arr))
31199
+ return map2;
31200
+ for (const raw of arr) {
31201
+ if (raw && typeof raw === "object") {
31202
+ const s = raw;
31203
+ const key = this.normalizeKey(this.keyOf(s));
31204
+ map2.set(key, s);
31205
+ }
31206
+ }
31207
+ return map2;
31208
+ }
31209
+ saveToDisk() {
31210
+ const dir = dirname(this.filePath);
31211
+ mkdirSync(dir, { recursive: true, mode: 448 });
31212
+ if (existsSync(this.filePath)) {
31213
+ try {
31214
+ chmodSync(this.filePath, 384);
31215
+ } catch {
31216
+ }
31217
+ }
31218
+ writeFileSync(this.filePath, this.serialize(), { mode: 384 });
31219
+ try {
31220
+ chmodSync(this.filePath, 384);
31221
+ chmodSync(dir, 448);
31222
+ } catch {
31223
+ }
31224
+ }
31225
+ /** Insert or replace a record, normalizing its key and marking it active. */
31226
+ add(session) {
31227
+ const key = this.normalizeKey(this.keyOf(session));
31228
+ this.sessions.set(key, session);
31229
+ this.mostRecentKey = key;
31230
+ this.saveToDisk();
31231
+ }
31232
+ /** Look up by key; with no key, returns the active (most-recent) session. */
31233
+ get(key) {
31234
+ if (key !== void 0)
31235
+ return this.sessions.get(this.normalizeKey(key)) ?? null;
31236
+ if (this.mostRecentKey !== null)
31237
+ return this.sessions.get(this.mostRecentKey) ?? null;
31238
+ return null;
31239
+ }
31240
+ /** The most-recently-added session, or `null`. */
31241
+ getActiveSession() {
31242
+ return this.get();
31243
+ }
31244
+ /** All sessions in insertion order. */
31245
+ list() {
31246
+ return Array.from(this.sessions.values());
31247
+ }
31248
+ /** Remove a session; fixes up the active pointer. Returns whether it existed. */
31249
+ remove(key) {
31250
+ const normalized = this.normalizeKey(key);
31251
+ const had = this.sessions.delete(normalized);
31252
+ if (had) {
31253
+ if (this.mostRecentKey === normalized) {
31254
+ const keys = Array.from(this.sessions.keys());
31255
+ this.mostRecentKey = keys[keys.length - 1] ?? null;
31256
+ }
31257
+ this.saveToDisk();
31258
+ }
31259
+ return had;
31260
+ }
31261
+ /** Clear in-memory state without touching disk. Test helper. */
31262
+ resetForTest() {
31263
+ this.sessions.clear();
31264
+ this.mostRecentKey = null;
31265
+ }
31266
+ };
31267
+ var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
31268
+
31269
+ // src/session-store.ts
31270
+ var SESSION_KEY = "vibo";
31271
+ function openStore() {
31272
+ return new SessionStore({
31273
+ filePath: readEnvVar("VIBO_SESSION_FILE") ?? join(homedir(), ".vibo-mcp", "session.json"),
31274
+ keyOf: (s) => typeof s.key === "string" && s.key ? s.key : SESSION_KEY,
31275
+ normalizeKey: (k) => k
31276
+ // fixed single key — no origin normalization
31277
+ });
31131
31278
  }
31132
31279
  function loadSession() {
31133
31280
  try {
31134
- const file2 = sessionFile();
31135
- if (!existsSync(file2)) return null;
31136
- const parsed = JSON.parse(readFileSync(file2, "utf8"));
31137
- if (parsed && typeof parsed.accessToken === "string" && parsed.accessToken) {
31138
- return { accessToken: parsed.accessToken, refreshToken: parsed.refreshToken ?? null };
31281
+ const rec = openStore().get(SESSION_KEY);
31282
+ if (rec && typeof rec.accessToken === "string" && rec.accessToken) {
31283
+ return {
31284
+ accessToken: rec.accessToken,
31285
+ refreshToken: typeof rec.refreshToken === "string" ? rec.refreshToken : null
31286
+ };
31139
31287
  }
31140
31288
  return null;
31141
31289
  } catch {
@@ -31143,14 +31291,11 @@ function loadSession() {
31143
31291
  }
31144
31292
  }
31145
31293
  function saveSession(session) {
31146
- const file2 = sessionFile();
31147
- mkdirSync(dirname(file2), { recursive: true, mode: 448 });
31148
- writeFileSync(
31149
- file2,
31150
- JSON.stringify({ accessToken: session.accessToken, refreshToken: session.refreshToken ?? null }, null, 2),
31151
- { mode: 384 }
31152
- );
31153
- chmodSync(file2, 384);
31294
+ openStore().add({
31295
+ key: SESSION_KEY,
31296
+ accessToken: session.accessToken,
31297
+ refreshToken: session.refreshToken ?? null
31298
+ });
31154
31299
  }
31155
31300
 
31156
31301
  // src/client.ts
@@ -1,21 +1,26 @@
1
1
  import { homedir } from 'os';
2
- import { dirname, join } from 'path';
3
- import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync, chmodSync } from 'fs';
2
+ import { join } from 'path';
4
3
  import { readEnvVar } from '@chrischall/mcp-utils';
5
- // Where a browser-captured token pair is persisted so it survives MCP restarts.
6
- // Override with VIBO_SESSION_FILE (used by tests). Written 0600 in a 0700 dir.
7
- function sessionFile() {
8
- return readEnvVar('VIBO_SESSION_FILE') ?? join(homedir(), '.vibo-mcp', 'session.json');
4
+ import { SessionStore } from '@chrischall/mcp-utils/session';
5
+ const SESSION_KEY = 'vibo';
6
+ // Constructed per call (it re-reads disk) so VIBO_SESSION_FILE is honored
7
+ // dynamically, matching the previous read-env-on-every-call behaviour.
8
+ function openStore() {
9
+ return new SessionStore({
10
+ filePath: readEnvVar('VIBO_SESSION_FILE') ?? join(homedir(), '.vibo-mcp', 'session.json'),
11
+ keyOf: (s) => (typeof s.key === 'string' && s.key ? s.key : SESSION_KEY),
12
+ normalizeKey: (k) => k, // fixed single key — no origin normalization
13
+ });
9
14
  }
10
15
  /** Load a previously-captured session, or null if none / unreadable. */
11
16
  export function loadSession() {
12
17
  try {
13
- const file = sessionFile();
14
- if (!existsSync(file))
15
- return null;
16
- const parsed = JSON.parse(readFileSync(file, 'utf8'));
17
- if (parsed && typeof parsed.accessToken === 'string' && parsed.accessToken) {
18
- return { accessToken: parsed.accessToken, refreshToken: parsed.refreshToken ?? null };
18
+ const rec = openStore().get(SESSION_KEY);
19
+ if (rec && typeof rec.accessToken === 'string' && rec.accessToken) {
20
+ return {
21
+ accessToken: rec.accessToken,
22
+ refreshToken: typeof rec.refreshToken === 'string' ? rec.refreshToken : null,
23
+ };
19
24
  }
20
25
  return null;
21
26
  }
@@ -25,19 +30,16 @@ export function loadSession() {
25
30
  }
26
31
  /** Persist a captured/refreshed token pair (0600 file in a 0700 dir). */
27
32
  export function saveSession(session) {
28
- const file = sessionFile();
29
- mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
30
- writeFileSync(file, JSON.stringify({ accessToken: session.accessToken, refreshToken: session.refreshToken ?? null }, null, 2), { mode: 0o600 });
31
- // `mode` in writeFileSync only applies when the file is *created*; enforce
32
- // 0600 on overwrite too (a pre-existing file keeps its old, possibly looser perms).
33
- chmodSync(file, 0o600);
33
+ openStore().add({
34
+ key: SESSION_KEY,
35
+ accessToken: session.accessToken,
36
+ refreshToken: session.refreshToken ?? null,
37
+ });
34
38
  }
35
39
  /** Remove any persisted session. */
36
40
  export function clearSession() {
37
41
  try {
38
- const file = sessionFile();
39
- if (existsSync(file))
40
- rmSync(file);
42
+ openStore().remove(SESSION_KEY);
41
43
  }
42
44
  catch {
43
45
  // best-effort
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.3.0'; // x-release-please-version
5
+ export const VERSION = '1.3.2'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibo-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
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>",
@@ -31,7 +31,7 @@
31
31
  "files": [
32
32
  "dist",
33
33
  ".claude-plugin",
34
- "SKILL.md",
34
+ "skills",
35
35
  ".mcp.json",
36
36
  "server.json"
37
37
  ],
@@ -44,14 +44,14 @@
44
44
  "test:coverage": "vitest run --coverage"
45
45
  },
46
46
  "dependencies": {
47
- "@chrischall/mcp-utils": "^0.10.4",
47
+ "@chrischall/mcp-utils": "^0.12.0",
48
48
  "@fetchproxy/bootstrap": "^1.3.4",
49
49
  "@modelcontextprotocol/sdk": "^1.29.0",
50
50
  "dotenv": "^17.4.0",
51
51
  "zod": "^4.4.2"
52
52
  },
53
53
  "devDependencies": {
54
- "@types/node": "^25.5.2",
54
+ "@types/node": "^26.0.0",
55
55
  "@vitest/coverage-v8": "^4.1.2",
56
56
  "esbuild": "^0.28.0",
57
57
  "typescript": "^6.0.2",
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.3.0",
9
+ "version": "1.3.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "vibo-mcp",
14
- "version": "1.3.0",
14
+ "version": "1.3.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
File without changes