vibo-mcp 1.2.0 → 1.3.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.
- package/.claude-plugin/marketplace.json +4 -4
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +1 -0
- package/SKILL.md +5 -0
- package/dist/auth.js +71 -0
- package/dist/bundle.js +285 -10
- package/dist/client.js +38 -3
- package/dist/index.js +2 -0
- package/dist/session-store.js +47 -0
- package/dist/tools/collaboration.js +2 -2
- package/dist/tools/session.js +24 -0
- package/dist/version.js +1 -1
- package/package.json +6 -5
- package/server.json +3 -3
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
"email": "chris.c.hall@gmail.com"
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
|
-
"description": "MCP server for Vibo (vibodj.com) — plan event music, song requests, and playlists via natural language",
|
|
10
|
-
"version": "1.
|
|
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.1"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
14
14
|
"name": "vibo-mcp",
|
|
15
15
|
"displayName": "Vibo",
|
|
16
16
|
"source": "./",
|
|
17
|
-
"description": "MCP server for Vibo — browse events
|
|
18
|
-
"version": "1.
|
|
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.1",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibo-mcp",
|
|
3
3
|
"displayName": "Vibo",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"description": "MCP server for Vibo (vibodj.com) — plan event music, song requests, and playlists via natural language",
|
|
4
|
+
"version": "1.3.1",
|
|
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",
|
|
8
8
|
"email": "chris.c.hall@gmail.com"
|
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ Choose one method:
|
|
|
33
33
|
|--------|----------|------|
|
|
34
34
|
| Email + password (recommended) | `VIBO_EMAIL`, `VIBO_PASSWORD` | You sign in to Vibo with an email/password. |
|
|
35
35
|
| Captured token | `VIBO_ACCESS_TOKEN` (+ `VIBO_REFRESH_TOKEN`) | Your account uses Apple/Google/Facebook sign-in (no password). Capture `x-token`/`x-refresh-token` from a signed-in `web.vibodj.com` session. |
|
|
36
|
+
| Browser capture (SSO) | run `vibo_capture_session` | With the fetchproxy browser extension installed and signed into `web.vibodj.com`, capture the token automatically (saved to `~/.vibo-mcp/session.json`). |
|
|
36
37
|
|
|
37
38
|
The server boots without credentials; the config error only surfaces on the
|
|
38
39
|
first tool call.
|
package/SKILL.md
CHANGED
|
@@ -40,6 +40,10 @@ Pick one:
|
|
|
40
40
|
- **Captured token (for Apple/Google/Facebook accounts):** set
|
|
41
41
|
`VIBO_ACCESS_TOKEN` (and `VIBO_REFRESH_TOKEN`) with values captured from a
|
|
42
42
|
signed-in `web.vibodj.com` session — no password needed.
|
|
43
|
+
- **Browser capture (SSO, automatic):** with the fetchproxy browser extension
|
|
44
|
+
installed and yourself signed into https://web.vibodj.com, run
|
|
45
|
+
`vibo_capture_session` once — it grabs the token from your tab (approve the
|
|
46
|
+
pair code), saves it to `~/.vibo-mcp/session.json`, and reuses it thereafter.
|
|
43
47
|
|
|
44
48
|
The server boots without credentials (so it can be installed and probed); the
|
|
45
49
|
config error only appears on the first tool call.
|
|
@@ -77,6 +81,7 @@ you get a dry-run preview of exactly what would be sent.
|
|
|
77
81
|
- `vibo_update_section` — edit a section's name, time, or note.
|
|
78
82
|
- `vibo_answer_question` — answer a planning question (text / option ids / link / image+file uploads).
|
|
79
83
|
- `vibo_set_profile_photo` — set your profile photo from a local image.
|
|
84
|
+
- `vibo_capture_session` — capture your login from a signed-in browser tab (SSO accounts).
|
|
80
85
|
- `vibo_mark_notifications_read`.
|
|
81
86
|
- `vibo_export_event_to_spotify` / `vibo_export_event_to_apple_music`.
|
|
82
87
|
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// SSO browser token auto-capture (fetchproxy bootstrap).
|
|
2
|
+
//
|
|
3
|
+
// Accounts that sign into Vibo only via Apple/Google/Facebook have no password
|
|
4
|
+
// to put in VIBO_PASSWORD. Instead of asking the user to dig the token out of
|
|
5
|
+
// DevTools and paste VIBO_ACCESS_TOKEN, this captures it once from their
|
|
6
|
+
// signed-in web.vibodj.com tab via the fetchproxy browser bridge, then operates
|
|
7
|
+
// from Node thereafter (the bridge touches only the one-time handshake).
|
|
8
|
+
//
|
|
9
|
+
// The Vibo web app stores its tokens as plain localStorage keys `x-token`
|
|
10
|
+
// (access) and `x-refresh-token` on the web.vibodj.com origin (verified live
|
|
11
|
+
// against a signed-in tab). We snapshot those two keys and return them; the caller verifies
|
|
12
|
+
// them (GET_ME) and only then persists via session-store. The client then uses
|
|
13
|
+
// them like any other token pair (with refresh-on-expiry).
|
|
14
|
+
//
|
|
15
|
+
// `@fetchproxy/bootstrap` is imported lazily so the default credential paths
|
|
16
|
+
// never load it — the .mcpb bundle externalizes it, and an eager import would
|
|
17
|
+
// crash the server at load. Tests inject a fake `bootstrap` via `deps`.
|
|
18
|
+
import { McpToolError } from '@chrischall/mcp-utils';
|
|
19
|
+
import { VERSION } from './version.js';
|
|
20
|
+
const SERVER_NAME = 'vibo-mcp';
|
|
21
|
+
/**
|
|
22
|
+
* Capture the signed-in user's Vibo token pair from their browser via the
|
|
23
|
+
* fetchproxy bridge and return it (the caller persists after verifying).
|
|
24
|
+
* Preconditions: the fetchproxy browser
|
|
25
|
+
* extension is installed and the user is signed into https://web.vibodj.com.
|
|
26
|
+
*/
|
|
27
|
+
export async function captureViboSession(deps = {}) {
|
|
28
|
+
let bootstrap = deps.bootstrap;
|
|
29
|
+
if (!bootstrap) {
|
|
30
|
+
try {
|
|
31
|
+
const mod = (await import('@fetchproxy/bootstrap'));
|
|
32
|
+
bootstrap = mod.bootstrap;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
throw new McpToolError('Browser token capture is unavailable in this build.', {
|
|
36
|
+
hint: 'Run vibo-mcp from npm (npx vibo-mcp) — the packaged .mcpb omits @fetchproxy/bootstrap. Or set VIBO_ACCESS_TOKEN instead.',
|
|
37
|
+
cause: err,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let session;
|
|
42
|
+
try {
|
|
43
|
+
session = await bootstrap({
|
|
44
|
+
serverName: SERVER_NAME,
|
|
45
|
+
version: VERSION,
|
|
46
|
+
domains: ['vibodj.com'],
|
|
47
|
+
storageSubdomain: 'web', // tokens live on web.vibodj.com
|
|
48
|
+
declare: { cookies: [], localStorage: ['x-token', 'x-refresh-token'], sessionStorage: [], captureHeaders: [] },
|
|
49
|
+
onPairCode: (code) => process.stderr.write(`[vibo-mcp] fetchproxy pair code: ${code}\n`),
|
|
50
|
+
onWaiting: (hint) => process.stderr.write(`[vibo-mcp] ${hint}\n`),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
55
|
+
throw new McpToolError(`Vibo browser token capture failed: ${msg}`, {
|
|
56
|
+
hint: 'Install the fetchproxy browser extension, sign into https://web.vibodj.com, approve the pair code, then retry.',
|
|
57
|
+
cause: err,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const accessToken = session.localStorage?.['x-token'];
|
|
61
|
+
const refreshToken = session.localStorage?.['x-refresh-token'] ?? null;
|
|
62
|
+
if (!accessToken) {
|
|
63
|
+
throw new McpToolError('No Vibo token found in the signed-in browser tab.', {
|
|
64
|
+
hint: 'Make sure you are signed into https://web.vibodj.com in the browser with the fetchproxy extension, then retry.',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// Return the captured pair WITHOUT persisting — the caller verifies it
|
|
68
|
+
// (GET_ME) before writing it to disk, so a stale snapshot never lands in
|
|
69
|
+
// session.json.
|
|
70
|
+
return { accessToken, refreshToken };
|
|
71
|
+
}
|
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,13 +31120,187 @@ function toolAnnotations(opts = {}) {
|
|
|
31116
31120
|
}
|
|
31117
31121
|
|
|
31118
31122
|
// src/version.ts
|
|
31119
|
-
var VERSION = "1.
|
|
31123
|
+
var VERSION = "1.3.1";
|
|
31120
31124
|
|
|
31121
31125
|
// src/client.ts
|
|
31122
|
-
import { dirname, join } from "path";
|
|
31126
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
31123
31127
|
import { fileURLToPath } from "url";
|
|
31124
|
-
|
|
31125
|
-
|
|
31128
|
+
|
|
31129
|
+
// src/session-store.ts
|
|
31130
|
+
import { homedir } from "os";
|
|
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
|
+
});
|
|
31278
|
+
}
|
|
31279
|
+
function loadSession() {
|
|
31280
|
+
try {
|
|
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
|
+
};
|
|
31287
|
+
}
|
|
31288
|
+
return null;
|
|
31289
|
+
} catch {
|
|
31290
|
+
return null;
|
|
31291
|
+
}
|
|
31292
|
+
}
|
|
31293
|
+
function saveSession(session) {
|
|
31294
|
+
openStore().add({
|
|
31295
|
+
key: SESSION_KEY,
|
|
31296
|
+
accessToken: session.accessToken,
|
|
31297
|
+
refreshToken: session.refreshToken ?? null
|
|
31298
|
+
});
|
|
31299
|
+
}
|
|
31300
|
+
|
|
31301
|
+
// src/client.ts
|
|
31302
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
31303
|
+
await loadDotenvSafely({ path: join2(__dirname, "..", ".env"), override: false });
|
|
31126
31304
|
var DEFAULT_API_URL = "https://api.vibodj.com/v2/graphql";
|
|
31127
31305
|
var SERVICE = "Vibo";
|
|
31128
31306
|
var SIGN_IN_HOST = "https://web.vibodj.com";
|
|
@@ -31148,6 +31326,7 @@ var ViboClient = class {
|
|
|
31148
31326
|
apiUrl;
|
|
31149
31327
|
email;
|
|
31150
31328
|
password;
|
|
31329
|
+
// Not readonly: cleared by setTokens() after a browser capture seeds a session.
|
|
31151
31330
|
configError;
|
|
31152
31331
|
accessToken;
|
|
31153
31332
|
refreshTokenValue;
|
|
@@ -31166,18 +31345,41 @@ var ViboClient = class {
|
|
|
31166
31345
|
this.accessToken = accessToken ?? null;
|
|
31167
31346
|
this.refreshTokenValue = refreshToken ?? null;
|
|
31168
31347
|
const haveLogin = Boolean(email3 && password);
|
|
31169
|
-
|
|
31348
|
+
if (!this.accessToken && !haveLogin) {
|
|
31349
|
+
const saved = loadSession();
|
|
31350
|
+
if (saved) {
|
|
31351
|
+
this.accessToken = saved.accessToken;
|
|
31352
|
+
this.refreshTokenValue = saved.refreshToken;
|
|
31353
|
+
}
|
|
31354
|
+
}
|
|
31355
|
+
const haveToken = Boolean(this.accessToken);
|
|
31170
31356
|
if (!haveLogin && !haveToken) {
|
|
31171
31357
|
this.configError = new McpToolError(
|
|
31172
31358
|
"Vibo credentials are not configured.",
|
|
31173
31359
|
{
|
|
31174
|
-
hint: "Set VIBO_EMAIL and VIBO_PASSWORD (recommended)
|
|
31360
|
+
hint: "Set VIBO_EMAIL and VIBO_PASSWORD (recommended); paste a captured VIBO_ACCESS_TOKEN (+ VIBO_REFRESH_TOKEN); or run vibo_capture_session to grab the token from your signed-in web.vibodj.com browser tab (Apple/Google/Facebook accounts)."
|
|
31175
31361
|
}
|
|
31176
31362
|
);
|
|
31177
31363
|
} else {
|
|
31178
31364
|
this.configError = null;
|
|
31179
31365
|
}
|
|
31180
31366
|
}
|
|
31367
|
+
/**
|
|
31368
|
+
* Adopt a browser-captured token pair (from vibo_capture_session) for
|
|
31369
|
+
* subsequent calls in this process and clear the config error so an account
|
|
31370
|
+
* that started with no credentials becomes usable. Does NOT persist — the
|
|
31371
|
+
* caller persists only after verifying the token authenticates (GET_ME).
|
|
31372
|
+
*/
|
|
31373
|
+
setTokens(accessToken, refreshToken) {
|
|
31374
|
+
this.accessToken = accessToken;
|
|
31375
|
+
this.refreshTokenValue = refreshToken;
|
|
31376
|
+
this.configError = null;
|
|
31377
|
+
}
|
|
31378
|
+
/** True when operating purely from a token (no email/password) — refreshed
|
|
31379
|
+
* tokens should be persisted so they survive a restart. */
|
|
31380
|
+
get tokenOnlyMode() {
|
|
31381
|
+
return !this.email || !this.password;
|
|
31382
|
+
}
|
|
31181
31383
|
/** Run a GraphQL operation, transparently authenticating + retrying once on token expiry. */
|
|
31182
31384
|
async gql(query, variables = {}) {
|
|
31183
31385
|
if (this.configError) throw this.configError;
|
|
@@ -31301,6 +31503,9 @@ var ViboClient = class {
|
|
|
31301
31503
|
if (data.refreshToken?.accessToken) {
|
|
31302
31504
|
this.accessToken = data.refreshToken.accessToken;
|
|
31303
31505
|
this.refreshTokenValue = data.refreshToken.refreshToken;
|
|
31506
|
+
if (this.tokenOnlyMode) {
|
|
31507
|
+
saveSession({ accessToken: this.accessToken, refreshToken: this.refreshTokenValue });
|
|
31508
|
+
}
|
|
31304
31509
|
return this.accessToken;
|
|
31305
31510
|
}
|
|
31306
31511
|
}
|
|
@@ -32466,12 +32671,12 @@ function registerCollaborationTools(server) {
|
|
|
32466
32671
|
server.registerTool(
|
|
32467
32672
|
"vibo_list_event_users",
|
|
32468
32673
|
{
|
|
32469
|
-
description: "List the hosts and guests on an event.",
|
|
32674
|
+
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.",
|
|
32470
32675
|
annotations: toolAnnotations({ title: "List Vibo event users", readOnly: true }),
|
|
32471
32676
|
inputSchema: {
|
|
32472
32677
|
eventId: external_exports.string().describe("Event id."),
|
|
32473
32678
|
usersType: external_exports.enum(["host", "guest"]).optional().describe("Filter to only hosts or only guests."),
|
|
32474
|
-
limit: limitSchema,
|
|
32679
|
+
limit: limitSchema.describe("Max items to return (default 20). Applies per group when usersType is omitted."),
|
|
32475
32680
|
skip: skipSchema
|
|
32476
32681
|
}
|
|
32477
32682
|
},
|
|
@@ -32616,6 +32821,75 @@ function registerUploadTools(server) {
|
|
|
32616
32821
|
);
|
|
32617
32822
|
}
|
|
32618
32823
|
|
|
32824
|
+
// src/auth.ts
|
|
32825
|
+
var SERVER_NAME = "vibo-mcp";
|
|
32826
|
+
async function captureViboSession(deps = {}) {
|
|
32827
|
+
let bootstrap = deps.bootstrap;
|
|
32828
|
+
if (!bootstrap) {
|
|
32829
|
+
try {
|
|
32830
|
+
const mod = await import("@fetchproxy/bootstrap");
|
|
32831
|
+
bootstrap = mod.bootstrap;
|
|
32832
|
+
} catch (err) {
|
|
32833
|
+
throw new McpToolError("Browser token capture is unavailable in this build.", {
|
|
32834
|
+
hint: "Run vibo-mcp from npm (npx vibo-mcp) \u2014 the packaged .mcpb omits @fetchproxy/bootstrap. Or set VIBO_ACCESS_TOKEN instead.",
|
|
32835
|
+
cause: err
|
|
32836
|
+
});
|
|
32837
|
+
}
|
|
32838
|
+
}
|
|
32839
|
+
let session;
|
|
32840
|
+
try {
|
|
32841
|
+
session = await bootstrap({
|
|
32842
|
+
serverName: SERVER_NAME,
|
|
32843
|
+
version: VERSION,
|
|
32844
|
+
domains: ["vibodj.com"],
|
|
32845
|
+
storageSubdomain: "web",
|
|
32846
|
+
// tokens live on web.vibodj.com
|
|
32847
|
+
declare: { cookies: [], localStorage: ["x-token", "x-refresh-token"], sessionStorage: [], captureHeaders: [] },
|
|
32848
|
+
onPairCode: (code) => process.stderr.write(`[vibo-mcp] fetchproxy pair code: ${code}
|
|
32849
|
+
`),
|
|
32850
|
+
onWaiting: (hint) => process.stderr.write(`[vibo-mcp] ${hint}
|
|
32851
|
+
`)
|
|
32852
|
+
});
|
|
32853
|
+
} catch (err) {
|
|
32854
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32855
|
+
throw new McpToolError(`Vibo browser token capture failed: ${msg}`, {
|
|
32856
|
+
hint: "Install the fetchproxy browser extension, sign into https://web.vibodj.com, approve the pair code, then retry.",
|
|
32857
|
+
cause: err
|
|
32858
|
+
});
|
|
32859
|
+
}
|
|
32860
|
+
const accessToken = session.localStorage?.["x-token"];
|
|
32861
|
+
const refreshToken = session.localStorage?.["x-refresh-token"] ?? null;
|
|
32862
|
+
if (!accessToken) {
|
|
32863
|
+
throw new McpToolError("No Vibo token found in the signed-in browser tab.", {
|
|
32864
|
+
hint: "Make sure you are signed into https://web.vibodj.com in the browser with the fetchproxy extension, then retry."
|
|
32865
|
+
});
|
|
32866
|
+
}
|
|
32867
|
+
return { accessToken, refreshToken };
|
|
32868
|
+
}
|
|
32869
|
+
|
|
32870
|
+
// src/tools/session.ts
|
|
32871
|
+
function registerSessionTools(server) {
|
|
32872
|
+
server.registerTool(
|
|
32873
|
+
"vibo_capture_session",
|
|
32874
|
+
{
|
|
32875
|
+
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.",
|
|
32876
|
+
annotations: toolAnnotations({ title: "Capture Vibo session (SSO)", readOnly: false })
|
|
32877
|
+
},
|
|
32878
|
+
async () => {
|
|
32879
|
+
const { accessToken, refreshToken } = await captureViboSession();
|
|
32880
|
+
client.setTokens(accessToken, refreshToken);
|
|
32881
|
+
const data = await client.gql(GET_ME);
|
|
32882
|
+
saveSession({ accessToken, refreshToken });
|
|
32883
|
+
return textResult({
|
|
32884
|
+
captured: true,
|
|
32885
|
+
hasRefreshToken: Boolean(refreshToken),
|
|
32886
|
+
userId: data.me._id,
|
|
32887
|
+
email: data.me.email
|
|
32888
|
+
});
|
|
32889
|
+
}
|
|
32890
|
+
);
|
|
32891
|
+
}
|
|
32892
|
+
|
|
32619
32893
|
// src/index.ts
|
|
32620
32894
|
await runMcp({
|
|
32621
32895
|
name: "vibo-mcp",
|
|
@@ -32635,6 +32909,7 @@ await runMcp({
|
|
|
32635
32909
|
registerImportTools,
|
|
32636
32910
|
registerCollaborationTools,
|
|
32637
32911
|
registerSectionEditTools,
|
|
32638
|
-
registerUploadTools
|
|
32912
|
+
registerUploadTools,
|
|
32913
|
+
registerSessionTools
|
|
32639
32914
|
]
|
|
32640
32915
|
});
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dirname, join } from 'path';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { loadDotenvSafely, readEnvVar, McpToolError, SessionNotAuthenticatedError, truncateErrorMessage, } from '@chrischall/mcp-utils';
|
|
4
|
+
import { loadSession, saveSession } from './session-store.js';
|
|
4
5
|
// Load .env for local dev; silently skip if dotenv is unavailable (e.g. the
|
|
5
6
|
// mcpb bundle, which externalizes dotenv). `override: false` means a
|
|
6
7
|
// host-provided env var always wins over .env.
|
|
@@ -50,6 +51,7 @@ export class ViboClient {
|
|
|
50
51
|
apiUrl;
|
|
51
52
|
email;
|
|
52
53
|
password;
|
|
54
|
+
// Not readonly: cleared by setTokens() after a browser capture seeds a session.
|
|
53
55
|
configError;
|
|
54
56
|
accessToken;
|
|
55
57
|
refreshTokenValue;
|
|
@@ -68,17 +70,45 @@ export class ViboClient {
|
|
|
68
70
|
this.accessToken = accessToken ?? null;
|
|
69
71
|
this.refreshTokenValue = refreshToken ?? null;
|
|
70
72
|
const haveLogin = Boolean(email && password);
|
|
71
|
-
|
|
73
|
+
// Fall back to a previously browser-captured session (SSO accounts) ONLY
|
|
74
|
+
// when there's no env token AND no email/password. Email/password is the
|
|
75
|
+
// documented preferred path and must win over a (possibly stale) saved
|
|
76
|
+
// session — otherwise an old session.json would silently shadow it.
|
|
77
|
+
if (!this.accessToken && !haveLogin) {
|
|
78
|
+
const saved = loadSession();
|
|
79
|
+
if (saved) {
|
|
80
|
+
this.accessToken = saved.accessToken;
|
|
81
|
+
this.refreshTokenValue = saved.refreshToken;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const haveToken = Boolean(this.accessToken);
|
|
72
85
|
if (!haveLogin && !haveToken) {
|
|
73
86
|
this.configError = new McpToolError('Vibo credentials are not configured.', {
|
|
74
|
-
hint: 'Set VIBO_EMAIL and VIBO_PASSWORD (recommended)
|
|
75
|
-
'VIBO_ACCESS_TOKEN (+ VIBO_REFRESH_TOKEN)
|
|
87
|
+
hint: 'Set VIBO_EMAIL and VIBO_PASSWORD (recommended); paste a captured ' +
|
|
88
|
+
'VIBO_ACCESS_TOKEN (+ VIBO_REFRESH_TOKEN); or run vibo_capture_session to grab the ' +
|
|
89
|
+
'token from your signed-in web.vibodj.com browser tab (Apple/Google/Facebook accounts).',
|
|
76
90
|
});
|
|
77
91
|
}
|
|
78
92
|
else {
|
|
79
93
|
this.configError = null;
|
|
80
94
|
}
|
|
81
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Adopt a browser-captured token pair (from vibo_capture_session) for
|
|
98
|
+
* subsequent calls in this process and clear the config error so an account
|
|
99
|
+
* that started with no credentials becomes usable. Does NOT persist — the
|
|
100
|
+
* caller persists only after verifying the token authenticates (GET_ME).
|
|
101
|
+
*/
|
|
102
|
+
setTokens(accessToken, refreshToken) {
|
|
103
|
+
this.accessToken = accessToken;
|
|
104
|
+
this.refreshTokenValue = refreshToken;
|
|
105
|
+
this.configError = null;
|
|
106
|
+
}
|
|
107
|
+
/** True when operating purely from a token (no email/password) — refreshed
|
|
108
|
+
* tokens should be persisted so they survive a restart. */
|
|
109
|
+
get tokenOnlyMode() {
|
|
110
|
+
return !this.email || !this.password;
|
|
111
|
+
}
|
|
82
112
|
/** Run a GraphQL operation, transparently authenticating + retrying once on token expiry. */
|
|
83
113
|
async gql(query, variables = {}) {
|
|
84
114
|
if (this.configError)
|
|
@@ -205,6 +235,11 @@ export class ViboClient {
|
|
|
205
235
|
if (data.refreshToken?.accessToken) {
|
|
206
236
|
this.accessToken = data.refreshToken.accessToken;
|
|
207
237
|
this.refreshTokenValue = data.refreshToken.refreshToken;
|
|
238
|
+
// Persist the rotated pair so a captured/pasted session survives
|
|
239
|
+
// a restart (no email/password to re-login with).
|
|
240
|
+
if (this.tokenOnlyMode) {
|
|
241
|
+
saveSession({ accessToken: this.accessToken, refreshToken: this.refreshTokenValue });
|
|
242
|
+
}
|
|
208
243
|
return this.accessToken;
|
|
209
244
|
}
|
|
210
245
|
}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { registerImportTools } from './tools/imports.js';
|
|
|
15
15
|
import { registerCollaborationTools } from './tools/collaboration.js';
|
|
16
16
|
import { registerSectionEditTools } from './tools/section-edit.js';
|
|
17
17
|
import { registerUploadTools } from './tools/uploads.js';
|
|
18
|
+
import { registerSessionTools } from './tools/session.js';
|
|
18
19
|
// The ViboClient is a module-level singleton (constructed in client.ts and
|
|
19
20
|
// imported by each tool module) that defers its config error to the first
|
|
20
21
|
// request. That preserves the deferred-config-error pattern: the server boots
|
|
@@ -39,5 +40,6 @@ await runMcp({
|
|
|
39
40
|
registerCollaborationTools,
|
|
40
41
|
registerSectionEditTools,
|
|
41
42
|
registerUploadTools,
|
|
43
|
+
registerSessionTools,
|
|
42
44
|
],
|
|
43
45
|
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { readEnvVar } from '@chrischall/mcp-utils';
|
|
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
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/** Load a previously-captured session, or null if none / unreadable. */
|
|
16
|
+
export function loadSession() {
|
|
17
|
+
try {
|
|
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
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Persist a captured/refreshed token pair (0600 file in a 0700 dir). */
|
|
32
|
+
export function saveSession(session) {
|
|
33
|
+
openStore().add({
|
|
34
|
+
key: SESSION_KEY,
|
|
35
|
+
accessToken: session.accessToken,
|
|
36
|
+
refreshToken: session.refreshToken ?? null,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Remove any persisted session. */
|
|
40
|
+
export function clearSession() {
|
|
41
|
+
try {
|
|
42
|
+
openStore().remove(SESSION_KEY);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// best-effort
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -5,12 +5,12 @@ import { LIST_EVENT_USERS, INVITE_USERS, CHANGE_USER_ROLE, REMOVE_USER } from '.
|
|
|
5
5
|
import { limitSchema, skipSchema, pagination, previewResult } from './shared.js';
|
|
6
6
|
export function registerCollaborationTools(server) {
|
|
7
7
|
server.registerTool('vibo_list_event_users', {
|
|
8
|
-
description:
|
|
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
9
|
annotations: toolAnnotations({ title: 'List Vibo event users', readOnly: true }),
|
|
10
10
|
inputSchema: {
|
|
11
11
|
eventId: z.string().describe('Event id.'),
|
|
12
12
|
usersType: z.enum(['host', 'guest']).optional().describe('Filter to only hosts or only guests.'),
|
|
13
|
-
limit: limitSchema,
|
|
13
|
+
limit: limitSchema.describe('Max items to return (default 20). Applies per group when usersType is omitted.'),
|
|
14
14
|
skip: skipSchema,
|
|
15
15
|
},
|
|
16
16
|
}, async ({ eventId, usersType, limit, skip }) => {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { textResult, toolAnnotations } from '@chrischall/mcp-utils';
|
|
2
|
+
import { client } from '../client.js';
|
|
3
|
+
import { captureViboSession } from '../auth.js';
|
|
4
|
+
import { saveSession } from '../session-store.js';
|
|
5
|
+
import { GET_ME } from '../gql.js';
|
|
6
|
+
export function registerSessionTools(server) {
|
|
7
|
+
server.registerTool('vibo_capture_session', {
|
|
8
|
+
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.",
|
|
9
|
+
annotations: toolAnnotations({ title: 'Capture Vibo session (SSO)', readOnly: false }),
|
|
10
|
+
}, async () => {
|
|
11
|
+
const { accessToken, refreshToken } = await captureViboSession();
|
|
12
|
+
client.setTokens(accessToken, refreshToken);
|
|
13
|
+
// Confirm the captured token actually authenticates BEFORE persisting it,
|
|
14
|
+
// so a stale snapshot never lands in session.json.
|
|
15
|
+
const data = await client.gql(GET_ME);
|
|
16
|
+
saveSession({ accessToken, refreshToken });
|
|
17
|
+
return textResult({
|
|
18
|
+
captured: true,
|
|
19
|
+
hasRefreshToken: Boolean(refreshToken),
|
|
20
|
+
userId: data.me._id,
|
|
21
|
+
email: data.me.email,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
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.
|
|
5
|
+
export const VERSION = '1.3.1'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibo-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"mcpName": "io.github.chrischall/vibo-mcp",
|
|
5
|
-
"description": "Vibo (vibodj.com) MCP server for Claude — host/couple event music planning. Developed and maintained by AI (Claude Code).",
|
|
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>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -37,20 +37,21 @@
|
|
|
37
37
|
],
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "tsc && npm run bundle",
|
|
40
|
-
"bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --outfile=dist/bundle.js",
|
|
40
|
+
"bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --external:@fetchproxy/bootstrap --outfile=dist/bundle.js",
|
|
41
41
|
"dev": "node dist/index.js",
|
|
42
42
|
"test": "vitest run",
|
|
43
43
|
"test:watch": "vitest",
|
|
44
44
|
"test:coverage": "vitest run --coverage"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@chrischall/mcp-utils": "^0.
|
|
47
|
+
"@chrischall/mcp-utils": "^0.12.0",
|
|
48
|
+
"@fetchproxy/bootstrap": "^1.3.4",
|
|
48
49
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
50
|
"dotenv": "^17.4.0",
|
|
50
51
|
"zod": "^4.4.2"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
|
-
"@types/node": "^
|
|
54
|
+
"@types/node": "^26.0.0",
|
|
54
55
|
"@vitest/coverage-v8": "^4.1.2",
|
|
55
56
|
"esbuild": "^0.28.0",
|
|
56
57
|
"typescript": "^6.0.2",
|
package/server.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.chrischall/vibo-mcp",
|
|
4
|
-
"description": "Vibo (vibodj.com) MCP — plan event music:
|
|
4
|
+
"description": "Vibo (vibodj.com) MCP — plan & manage event music: timeline, songs, ideas, questions, guests",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/chrischall/vibo-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.3.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "vibo-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.3.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|