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.
@@ -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.2.0"
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 and timelines, add/like song requests, and export to Spotify/Apple Music",
18
- "version": "1.2.0",
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.2.0",
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.2.0";
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
- var __dirname = dirname(fileURLToPath(import.meta.url));
31125
- await loadDotenvSafely({ path: join(__dirname, "..", ".env"), override: false });
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
- const haveToken = Boolean(accessToken);
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), or paste a captured VIBO_ACCESS_TOKEN (+ VIBO_REFRESH_TOKEN) if your account uses Apple/Google/Facebook sign-in."
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
- const haveToken = Boolean(accessToken);
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), or paste a captured ' +
75
- 'VIBO_ACCESS_TOKEN (+ VIBO_REFRESH_TOKEN) if your account uses Apple/Google/Facebook sign-in.',
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: 'List the hosts and guests on an event.',
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.2.0'; // x-release-please-version
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.2.0",
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: events, timeline, song requests, playlist export",
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.2.0",
9
+ "version": "1.3.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "vibo-mcp",
14
- "version": "1.2.0",
14
+ "version": "1.3.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },