vibo-mcp 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +139 -9
- package/dist/client.js +38 -3
- package/dist/index.js +2 -0
- package/dist/session-store.js +45 -0
- package/dist/tools/collaboration.js +2 -2
- package/dist/tools/session.js +24 -0
- package/dist/version.js +1 -1
- package/package.json +4 -3
- 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.0"
|
|
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.0",
|
|
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.0",
|
|
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
|
@@ -31116,13 +31116,46 @@ function toolAnnotations(opts = {}) {
|
|
|
31116
31116
|
}
|
|
31117
31117
|
|
|
31118
31118
|
// src/version.ts
|
|
31119
|
-
var VERSION = "1.
|
|
31119
|
+
var VERSION = "1.3.0";
|
|
31120
31120
|
|
|
31121
31121
|
// src/client.ts
|
|
31122
|
-
import { dirname, join } from "path";
|
|
31122
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
31123
31123
|
import { fileURLToPath } from "url";
|
|
31124
|
-
|
|
31125
|
-
|
|
31124
|
+
|
|
31125
|
+
// src/session-store.ts
|
|
31126
|
+
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
|
+
}
|
|
31132
|
+
function loadSession() {
|
|
31133
|
+
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 };
|
|
31139
|
+
}
|
|
31140
|
+
return null;
|
|
31141
|
+
} catch {
|
|
31142
|
+
return null;
|
|
31143
|
+
}
|
|
31144
|
+
}
|
|
31145
|
+
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);
|
|
31154
|
+
}
|
|
31155
|
+
|
|
31156
|
+
// src/client.ts
|
|
31157
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
31158
|
+
await loadDotenvSafely({ path: join2(__dirname, "..", ".env"), override: false });
|
|
31126
31159
|
var DEFAULT_API_URL = "https://api.vibodj.com/v2/graphql";
|
|
31127
31160
|
var SERVICE = "Vibo";
|
|
31128
31161
|
var SIGN_IN_HOST = "https://web.vibodj.com";
|
|
@@ -31148,6 +31181,7 @@ var ViboClient = class {
|
|
|
31148
31181
|
apiUrl;
|
|
31149
31182
|
email;
|
|
31150
31183
|
password;
|
|
31184
|
+
// Not readonly: cleared by setTokens() after a browser capture seeds a session.
|
|
31151
31185
|
configError;
|
|
31152
31186
|
accessToken;
|
|
31153
31187
|
refreshTokenValue;
|
|
@@ -31166,18 +31200,41 @@ var ViboClient = class {
|
|
|
31166
31200
|
this.accessToken = accessToken ?? null;
|
|
31167
31201
|
this.refreshTokenValue = refreshToken ?? null;
|
|
31168
31202
|
const haveLogin = Boolean(email3 && password);
|
|
31169
|
-
|
|
31203
|
+
if (!this.accessToken && !haveLogin) {
|
|
31204
|
+
const saved = loadSession();
|
|
31205
|
+
if (saved) {
|
|
31206
|
+
this.accessToken = saved.accessToken;
|
|
31207
|
+
this.refreshTokenValue = saved.refreshToken;
|
|
31208
|
+
}
|
|
31209
|
+
}
|
|
31210
|
+
const haveToken = Boolean(this.accessToken);
|
|
31170
31211
|
if (!haveLogin && !haveToken) {
|
|
31171
31212
|
this.configError = new McpToolError(
|
|
31172
31213
|
"Vibo credentials are not configured.",
|
|
31173
31214
|
{
|
|
31174
|
-
hint: "Set VIBO_EMAIL and VIBO_PASSWORD (recommended)
|
|
31215
|
+
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
31216
|
}
|
|
31176
31217
|
);
|
|
31177
31218
|
} else {
|
|
31178
31219
|
this.configError = null;
|
|
31179
31220
|
}
|
|
31180
31221
|
}
|
|
31222
|
+
/**
|
|
31223
|
+
* Adopt a browser-captured token pair (from vibo_capture_session) for
|
|
31224
|
+
* subsequent calls in this process and clear the config error so an account
|
|
31225
|
+
* that started with no credentials becomes usable. Does NOT persist — the
|
|
31226
|
+
* caller persists only after verifying the token authenticates (GET_ME).
|
|
31227
|
+
*/
|
|
31228
|
+
setTokens(accessToken, refreshToken) {
|
|
31229
|
+
this.accessToken = accessToken;
|
|
31230
|
+
this.refreshTokenValue = refreshToken;
|
|
31231
|
+
this.configError = null;
|
|
31232
|
+
}
|
|
31233
|
+
/** True when operating purely from a token (no email/password) — refreshed
|
|
31234
|
+
* tokens should be persisted so they survive a restart. */
|
|
31235
|
+
get tokenOnlyMode() {
|
|
31236
|
+
return !this.email || !this.password;
|
|
31237
|
+
}
|
|
31181
31238
|
/** Run a GraphQL operation, transparently authenticating + retrying once on token expiry. */
|
|
31182
31239
|
async gql(query, variables = {}) {
|
|
31183
31240
|
if (this.configError) throw this.configError;
|
|
@@ -31301,6 +31358,9 @@ var ViboClient = class {
|
|
|
31301
31358
|
if (data.refreshToken?.accessToken) {
|
|
31302
31359
|
this.accessToken = data.refreshToken.accessToken;
|
|
31303
31360
|
this.refreshTokenValue = data.refreshToken.refreshToken;
|
|
31361
|
+
if (this.tokenOnlyMode) {
|
|
31362
|
+
saveSession({ accessToken: this.accessToken, refreshToken: this.refreshTokenValue });
|
|
31363
|
+
}
|
|
31304
31364
|
return this.accessToken;
|
|
31305
31365
|
}
|
|
31306
31366
|
}
|
|
@@ -32466,12 +32526,12 @@ function registerCollaborationTools(server) {
|
|
|
32466
32526
|
server.registerTool(
|
|
32467
32527
|
"vibo_list_event_users",
|
|
32468
32528
|
{
|
|
32469
|
-
description: "List the hosts and guests on an event.",
|
|
32529
|
+
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
32530
|
annotations: toolAnnotations({ title: "List Vibo event users", readOnly: true }),
|
|
32471
32531
|
inputSchema: {
|
|
32472
32532
|
eventId: external_exports.string().describe("Event id."),
|
|
32473
32533
|
usersType: external_exports.enum(["host", "guest"]).optional().describe("Filter to only hosts or only guests."),
|
|
32474
|
-
limit: limitSchema,
|
|
32534
|
+
limit: limitSchema.describe("Max items to return (default 20). Applies per group when usersType is omitted."),
|
|
32475
32535
|
skip: skipSchema
|
|
32476
32536
|
}
|
|
32477
32537
|
},
|
|
@@ -32616,6 +32676,75 @@ function registerUploadTools(server) {
|
|
|
32616
32676
|
);
|
|
32617
32677
|
}
|
|
32618
32678
|
|
|
32679
|
+
// src/auth.ts
|
|
32680
|
+
var SERVER_NAME = "vibo-mcp";
|
|
32681
|
+
async function captureViboSession(deps = {}) {
|
|
32682
|
+
let bootstrap = deps.bootstrap;
|
|
32683
|
+
if (!bootstrap) {
|
|
32684
|
+
try {
|
|
32685
|
+
const mod = await import("@fetchproxy/bootstrap");
|
|
32686
|
+
bootstrap = mod.bootstrap;
|
|
32687
|
+
} catch (err) {
|
|
32688
|
+
throw new McpToolError("Browser token capture is unavailable in this build.", {
|
|
32689
|
+
hint: "Run vibo-mcp from npm (npx vibo-mcp) \u2014 the packaged .mcpb omits @fetchproxy/bootstrap. Or set VIBO_ACCESS_TOKEN instead.",
|
|
32690
|
+
cause: err
|
|
32691
|
+
});
|
|
32692
|
+
}
|
|
32693
|
+
}
|
|
32694
|
+
let session;
|
|
32695
|
+
try {
|
|
32696
|
+
session = await bootstrap({
|
|
32697
|
+
serverName: SERVER_NAME,
|
|
32698
|
+
version: VERSION,
|
|
32699
|
+
domains: ["vibodj.com"],
|
|
32700
|
+
storageSubdomain: "web",
|
|
32701
|
+
// tokens live on web.vibodj.com
|
|
32702
|
+
declare: { cookies: [], localStorage: ["x-token", "x-refresh-token"], sessionStorage: [], captureHeaders: [] },
|
|
32703
|
+
onPairCode: (code) => process.stderr.write(`[vibo-mcp] fetchproxy pair code: ${code}
|
|
32704
|
+
`),
|
|
32705
|
+
onWaiting: (hint) => process.stderr.write(`[vibo-mcp] ${hint}
|
|
32706
|
+
`)
|
|
32707
|
+
});
|
|
32708
|
+
} catch (err) {
|
|
32709
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32710
|
+
throw new McpToolError(`Vibo browser token capture failed: ${msg}`, {
|
|
32711
|
+
hint: "Install the fetchproxy browser extension, sign into https://web.vibodj.com, approve the pair code, then retry.",
|
|
32712
|
+
cause: err
|
|
32713
|
+
});
|
|
32714
|
+
}
|
|
32715
|
+
const accessToken = session.localStorage?.["x-token"];
|
|
32716
|
+
const refreshToken = session.localStorage?.["x-refresh-token"] ?? null;
|
|
32717
|
+
if (!accessToken) {
|
|
32718
|
+
throw new McpToolError("No Vibo token found in the signed-in browser tab.", {
|
|
32719
|
+
hint: "Make sure you are signed into https://web.vibodj.com in the browser with the fetchproxy extension, then retry."
|
|
32720
|
+
});
|
|
32721
|
+
}
|
|
32722
|
+
return { accessToken, refreshToken };
|
|
32723
|
+
}
|
|
32724
|
+
|
|
32725
|
+
// src/tools/session.ts
|
|
32726
|
+
function registerSessionTools(server) {
|
|
32727
|
+
server.registerTool(
|
|
32728
|
+
"vibo_capture_session",
|
|
32729
|
+
{
|
|
32730
|
+
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.",
|
|
32731
|
+
annotations: toolAnnotations({ title: "Capture Vibo session (SSO)", readOnly: false })
|
|
32732
|
+
},
|
|
32733
|
+
async () => {
|
|
32734
|
+
const { accessToken, refreshToken } = await captureViboSession();
|
|
32735
|
+
client.setTokens(accessToken, refreshToken);
|
|
32736
|
+
const data = await client.gql(GET_ME);
|
|
32737
|
+
saveSession({ accessToken, refreshToken });
|
|
32738
|
+
return textResult({
|
|
32739
|
+
captured: true,
|
|
32740
|
+
hasRefreshToken: Boolean(refreshToken),
|
|
32741
|
+
userId: data.me._id,
|
|
32742
|
+
email: data.me.email
|
|
32743
|
+
});
|
|
32744
|
+
}
|
|
32745
|
+
);
|
|
32746
|
+
}
|
|
32747
|
+
|
|
32619
32748
|
// src/index.ts
|
|
32620
32749
|
await runMcp({
|
|
32621
32750
|
name: "vibo-mcp",
|
|
@@ -32635,6 +32764,7 @@ await runMcp({
|
|
|
32635
32764
|
registerImportTools,
|
|
32636
32765
|
registerCollaborationTools,
|
|
32637
32766
|
registerSectionEditTools,
|
|
32638
|
-
registerUploadTools
|
|
32767
|
+
registerUploadTools,
|
|
32768
|
+
registerSessionTools
|
|
32639
32769
|
]
|
|
32640
32770
|
});
|
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,45 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync, chmodSync } from 'fs';
|
|
4
|
+
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');
|
|
9
|
+
}
|
|
10
|
+
/** Load a previously-captured session, or null if none / unreadable. */
|
|
11
|
+
export function loadSession() {
|
|
12
|
+
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 };
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Persist a captured/refreshed token pair (0600 file in a 0700 dir). */
|
|
27
|
+
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);
|
|
34
|
+
}
|
|
35
|
+
/** Remove any persisted session. */
|
|
36
|
+
export function clearSession() {
|
|
37
|
+
try {
|
|
38
|
+
const file = sessionFile();
|
|
39
|
+
if (existsSync(file))
|
|
40
|
+
rmSync(file);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// best-effort
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -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.0'; // 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.0",
|
|
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,7 +37,7 @@
|
|
|
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",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@chrischall/mcp-utils": "^0.10.4",
|
|
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"
|
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.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "vibo-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.3.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|