vibo-mcp 1.7.1 → 1.8.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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +200 -45
- package/dist/tools/collaboration.js +8 -3
- package/dist/tools/ideas.js +4 -2
- package/dist/tools/notifications.js +4 -2
- package/dist/tools/profile.js +4 -2
- package/dist/tools/songs.js +3 -2
- package/dist/version.js +1 -1
- package/package.json +6 -6
- package/server.json +2 -2
- package/skills/vibo-mcp/SKILL.md +35 -40
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for Vibo (vibodj.com) — plan & manage event music, song requests, ideas, guests, and playlists via natural language",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.8.1"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "Vibo",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "MCP server for Vibo — browse & manage events, timeline, songs, the DJ song ideas/questions, guests, and exports to Spotify/Apple Music",
|
|
18
|
-
"version": "1.
|
|
18
|
+
"version": "1.8.1",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibo-mcp",
|
|
3
3
|
"displayName": "Vibo",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.8.1",
|
|
5
5
|
"description": "MCP server for Vibo (vibodj.com) — plan & manage event music, song requests, ideas, guests, and playlists via natural language",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
package/dist/bundle.js
CHANGED
|
@@ -13450,7 +13450,7 @@ ${content.join("\n")}
|
|
|
13450
13450
|
var version = {
|
|
13451
13451
|
major: 4,
|
|
13452
13452
|
minor: 5,
|
|
13453
|
-
patch:
|
|
13453
|
+
patch: 4
|
|
13454
13454
|
};
|
|
13455
13455
|
|
|
13456
13456
|
// node_modules/zod/v4/core/schemas.js
|
|
@@ -15743,22 +15743,99 @@ function isRecursive(inst, stack) {
|
|
|
15743
15743
|
result = true;
|
|
15744
15744
|
};
|
|
15745
15745
|
const def = inst._zod.def;
|
|
15746
|
-
|
|
15747
|
-
|
|
15748
|
-
|
|
15749
|
-
|
|
15750
|
-
|
|
15751
|
-
|
|
15752
|
-
|
|
15753
|
-
|
|
15754
|
-
|
|
15755
|
-
|
|
15756
|
-
|
|
15757
|
-
|
|
15758
|
-
|
|
15759
|
-
|
|
15760
|
-
|
|
15761
|
-
|
|
15746
|
+
const kind = def.type;
|
|
15747
|
+
switch (kind) {
|
|
15748
|
+
case "object": {
|
|
15749
|
+
for (const key of Reflect.ownKeys(def.shape))
|
|
15750
|
+
check2(def.shape[key]);
|
|
15751
|
+
check2(def.catchall);
|
|
15752
|
+
break;
|
|
15753
|
+
}
|
|
15754
|
+
case "array":
|
|
15755
|
+
check2(def.element);
|
|
15756
|
+
break;
|
|
15757
|
+
case "tuple":
|
|
15758
|
+
for (const el of def.items)
|
|
15759
|
+
check2(el);
|
|
15760
|
+
check2(def.rest);
|
|
15761
|
+
break;
|
|
15762
|
+
case "record":
|
|
15763
|
+
case "map":
|
|
15764
|
+
check2(def.keyType);
|
|
15765
|
+
check2(def.valueType);
|
|
15766
|
+
break;
|
|
15767
|
+
case "set":
|
|
15768
|
+
check2(def.valueType);
|
|
15769
|
+
break;
|
|
15770
|
+
case "union":
|
|
15771
|
+
for (const el of def.options)
|
|
15772
|
+
check2(el);
|
|
15773
|
+
break;
|
|
15774
|
+
case "intersection":
|
|
15775
|
+
check2(def.left);
|
|
15776
|
+
check2(def.right);
|
|
15777
|
+
break;
|
|
15778
|
+
case "optional":
|
|
15779
|
+
case "nullable":
|
|
15780
|
+
case "default":
|
|
15781
|
+
case "prefault":
|
|
15782
|
+
case "catch":
|
|
15783
|
+
case "readonly":
|
|
15784
|
+
case "nonoptional":
|
|
15785
|
+
case "promise":
|
|
15786
|
+
case "success":
|
|
15787
|
+
check2(def.innerType);
|
|
15788
|
+
break;
|
|
15789
|
+
case "pipe":
|
|
15790
|
+
check2(def.in);
|
|
15791
|
+
check2(def.out);
|
|
15792
|
+
break;
|
|
15793
|
+
case "function":
|
|
15794
|
+
check2(def.input);
|
|
15795
|
+
check2(def.output);
|
|
15796
|
+
break;
|
|
15797
|
+
// reading `_zod.innerType` resolves the getter once and caches it
|
|
15798
|
+
case "lazy":
|
|
15799
|
+
check2(inst._zod.innerType);
|
|
15800
|
+
break;
|
|
15801
|
+
// a leaf by choice: `parts` are regex fragments, not data positions
|
|
15802
|
+
case "template_literal":
|
|
15803
|
+
// leaves
|
|
15804
|
+
case "string":
|
|
15805
|
+
case "number":
|
|
15806
|
+
case "int":
|
|
15807
|
+
case "boolean":
|
|
15808
|
+
case "bigint":
|
|
15809
|
+
case "symbol":
|
|
15810
|
+
case "undefined":
|
|
15811
|
+
case "null":
|
|
15812
|
+
case "void":
|
|
15813
|
+
case "never":
|
|
15814
|
+
case "any":
|
|
15815
|
+
case "unknown":
|
|
15816
|
+
case "date":
|
|
15817
|
+
case "nan":
|
|
15818
|
+
case "enum":
|
|
15819
|
+
case "literal":
|
|
15820
|
+
case "file":
|
|
15821
|
+
case "transform":
|
|
15822
|
+
case "custom":
|
|
15823
|
+
break;
|
|
15824
|
+
default: {
|
|
15825
|
+
kind;
|
|
15826
|
+
for (const key in def) {
|
|
15827
|
+
const desc = Object.getOwnPropertyDescriptor(def, key);
|
|
15828
|
+
if (!desc || desc.get)
|
|
15829
|
+
continue;
|
|
15830
|
+
const value = desc.value;
|
|
15831
|
+
if (!value || typeof value !== "object")
|
|
15832
|
+
continue;
|
|
15833
|
+
if (value._zod)
|
|
15834
|
+
check2(value);
|
|
15835
|
+
else if (Array.isArray(value))
|
|
15836
|
+
for (const el of value)
|
|
15837
|
+
check2(el);
|
|
15838
|
+
}
|
|
15762
15839
|
}
|
|
15763
15840
|
}
|
|
15764
15841
|
stack.delete(inst);
|
|
@@ -25891,6 +25968,7 @@ function initializeContext(params) {
|
|
|
25891
25968
|
cycles: params?.cycles ?? "ref",
|
|
25892
25969
|
reused: params?.reused ?? "inline",
|
|
25893
25970
|
intersections: [],
|
|
25971
|
+
deferred: [],
|
|
25894
25972
|
external: params?.external ?? void 0
|
|
25895
25973
|
};
|
|
25896
25974
|
}
|
|
@@ -26239,6 +26317,8 @@ function finalize(ctx, schema) {
|
|
|
26239
26317
|
compactTypeUnion(entry[1].def ?? entry[1].schema);
|
|
26240
26318
|
}
|
|
26241
26319
|
}
|
|
26320
|
+
for (const rewrite of ctx.deferred)
|
|
26321
|
+
rewrite();
|
|
26242
26322
|
if (ctx.intersections.length) {
|
|
26243
26323
|
const carriers = /* @__PURE__ */ new Map();
|
|
26244
26324
|
for (const seen of ctx.seen.values()) {
|
|
@@ -26415,12 +26495,12 @@ var stringProcessor = (schema, ctx, _json, _params) => {
|
|
|
26415
26495
|
if (contentEncoding)
|
|
26416
26496
|
json2.contentEncoding = contentEncoding;
|
|
26417
26497
|
if (patterns && patterns.size > 0) {
|
|
26418
|
-
const
|
|
26419
|
-
if (
|
|
26420
|
-
json2.pattern =
|
|
26421
|
-
else if (
|
|
26498
|
+
const patternList = [...patterns];
|
|
26499
|
+
if (patternList.length === 1)
|
|
26500
|
+
json2.pattern = patternList[0].source;
|
|
26501
|
+
else if (patternList.length > 1) {
|
|
26422
26502
|
json2.allOf = [
|
|
26423
|
-
...
|
|
26503
|
+
...patternList.map((regex) => ({
|
|
26424
26504
|
...ctx.target === "draft-07" || ctx.target === "draft-04" || ctx.target === "openapi-3.0" ? { type: "string" } : {},
|
|
26425
26505
|
pattern: regex.source
|
|
26426
26506
|
}))
|
|
@@ -26764,6 +26844,69 @@ var tupleProcessor = (schema, ctx, _json, params) => {
|
|
|
26764
26844
|
if (typeof maximum === "number")
|
|
26765
26845
|
json2.maxItems = maximum;
|
|
26766
26846
|
};
|
|
26847
|
+
function stringifyKeyNames(bySchema, json2, visited) {
|
|
26848
|
+
if (json2.$ref) {
|
|
26849
|
+
if (visited.has(json2))
|
|
26850
|
+
return json2;
|
|
26851
|
+
visited.add(json2);
|
|
26852
|
+
const def = bySchema.get(json2)?.def;
|
|
26853
|
+
if (!def)
|
|
26854
|
+
return json2;
|
|
26855
|
+
const inlined = stringifyKeyNames(bySchema, def, visited);
|
|
26856
|
+
return inlined === def ? json2 : inlined;
|
|
26857
|
+
}
|
|
26858
|
+
for (const keyword of ["anyOf", "oneOf"]) {
|
|
26859
|
+
const branches = json2[keyword];
|
|
26860
|
+
if (!Array.isArray(branches))
|
|
26861
|
+
continue;
|
|
26862
|
+
const mapped = branches.map((branch) => stringifyKeyNames(bySchema, branch, visited));
|
|
26863
|
+
if (mapped.some((branch, i) => branch !== branches[i]))
|
|
26864
|
+
json2 = { ...json2, [keyword]: mapped };
|
|
26865
|
+
}
|
|
26866
|
+
const types = Array.isArray(json2.type) ? json2.type : [json2.type];
|
|
26867
|
+
const numericType = !types.includes("string") && types.some((t) => t === "number" || t === "integer");
|
|
26868
|
+
const values = json2.enum ?? (json2.const !== void 0 ? [json2.const] : void 0);
|
|
26869
|
+
if (!numericType && !values?.some((v) => typeof v === "number"))
|
|
26870
|
+
return json2;
|
|
26871
|
+
const { minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, format, id, ...rest } = json2;
|
|
26872
|
+
if (rest.enum)
|
|
26873
|
+
rest.enum = rest.enum.map((v) => typeof v === "number" ? String(v) : v);
|
|
26874
|
+
else if (typeof rest.const === "number")
|
|
26875
|
+
rest.const = String(rest.const);
|
|
26876
|
+
if (!numericType)
|
|
26877
|
+
return rest;
|
|
26878
|
+
rest.type = "string";
|
|
26879
|
+
if (!values)
|
|
26880
|
+
rest.pattern = (types.includes("number") ? number : integer).source;
|
|
26881
|
+
return rest;
|
|
26882
|
+
}
|
|
26883
|
+
var pendingRecords = /* @__PURE__ */ new WeakMap();
|
|
26884
|
+
function rewriteKeyNames(ctx) {
|
|
26885
|
+
const bySchema = /* @__PURE__ */ new Map();
|
|
26886
|
+
for (const entry of ctx.seen.values()) {
|
|
26887
|
+
if (entry.def && !bySchema.has(entry.schema))
|
|
26888
|
+
bySchema.set(entry.schema, entry);
|
|
26889
|
+
}
|
|
26890
|
+
const rewrites = /* @__PURE__ */ new Map();
|
|
26891
|
+
for (const record2 of pendingRecords.get(ctx) ?? []) {
|
|
26892
|
+
const seen = ctx.seen.get(record2);
|
|
26893
|
+
const names = (seen?.def ?? seen?.schema)?.propertyNames;
|
|
26894
|
+
if (!names || names === true || rewrites.has(names))
|
|
26895
|
+
continue;
|
|
26896
|
+
const rewritten = stringifyKeyNames(bySchema, names, /* @__PURE__ */ new Set());
|
|
26897
|
+
if (rewritten !== names)
|
|
26898
|
+
rewrites.set(names, rewritten);
|
|
26899
|
+
}
|
|
26900
|
+
if (!rewrites.size)
|
|
26901
|
+
return;
|
|
26902
|
+
for (const entry of ctx.seen.values()) {
|
|
26903
|
+
for (const carrier of [entry.schema, entry.def]) {
|
|
26904
|
+
const rewritten = carrier && rewrites.get(carrier.propertyNames);
|
|
26905
|
+
if (rewritten)
|
|
26906
|
+
carrier.propertyNames = rewritten;
|
|
26907
|
+
}
|
|
26908
|
+
}
|
|
26909
|
+
}
|
|
26767
26910
|
var recordProcessor = (schema, ctx, _json, params) => {
|
|
26768
26911
|
const json2 = _json;
|
|
26769
26912
|
const def = schema._zod.def;
|
|
@@ -26786,6 +26929,13 @@ var recordProcessor = (schema, ctx, _json, params) => {
|
|
|
26786
26929
|
...params,
|
|
26787
26930
|
path: [...params.path, "propertyNames"]
|
|
26788
26931
|
});
|
|
26932
|
+
let pending = pendingRecords.get(ctx);
|
|
26933
|
+
if (!pending) {
|
|
26934
|
+
pending = [];
|
|
26935
|
+
pendingRecords.set(ctx, pending);
|
|
26936
|
+
ctx.deferred.push(() => rewriteKeyNames(ctx));
|
|
26937
|
+
}
|
|
26938
|
+
pending.push(schema);
|
|
26789
26939
|
}
|
|
26790
26940
|
json2.additionalProperties = process2(def.valueType, ctx, {
|
|
26791
26941
|
...params,
|
|
@@ -26797,7 +26947,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
|
|
|
26797
26947
|
if (keyValues && !def.partial && !omittableOnInput) {
|
|
26798
26948
|
const validKeyValues = [...keyValues].filter((v) => typeof v === "string" || typeof v === "number");
|
|
26799
26949
|
if (validKeyValues.length > 0) {
|
|
26800
|
-
json2.required = validKeyValues;
|
|
26950
|
+
json2.required = validKeyValues.map(String);
|
|
26801
26951
|
}
|
|
26802
26952
|
}
|
|
26803
26953
|
};
|
|
@@ -35790,7 +35940,7 @@ function toolAnnotations(opts = {}) {
|
|
|
35790
35940
|
}
|
|
35791
35941
|
|
|
35792
35942
|
// src/version.ts
|
|
35793
|
-
var VERSION = "1.
|
|
35943
|
+
var VERSION = "1.8.1";
|
|
35794
35944
|
|
|
35795
35945
|
// src/client.ts
|
|
35796
35946
|
import { dirname as dirname2, join as join3 } from "path";
|
|
@@ -36569,17 +36719,27 @@ var UPLOAD_USER_PHOTO = `
|
|
|
36569
36719
|
}
|
|
36570
36720
|
`;
|
|
36571
36721
|
|
|
36722
|
+
// src/view.ts
|
|
36723
|
+
var VIBO_VIEWS = ["compact", "full"];
|
|
36724
|
+
var NOTE = `compact strips image/avatar URLs from the response; "full" returns Vibo's payload untouched. No field projection: this server has no verified record of which Vibo fields matter, and inventing one would risk dropping a field a caller needs.`;
|
|
36725
|
+
var viewArg = () => viewParam(VIBO_VIEWS, { note: NOTE });
|
|
36726
|
+
function viewResponse(view, data) {
|
|
36727
|
+
const rung = resolveView(view, VIBO_VIEWS);
|
|
36728
|
+
return minifiedResult(rung === "compact" ? stripMediaUrls(data) : data);
|
|
36729
|
+
}
|
|
36730
|
+
|
|
36572
36731
|
// src/tools/profile.ts
|
|
36573
36732
|
function registerProfileTools(server, client2) {
|
|
36574
36733
|
server.registerTool(
|
|
36575
36734
|
"vibo_get_me",
|
|
36576
36735
|
{
|
|
36577
36736
|
description: "Get the signed-in Vibo user's profile (id, name, email, phone, locale, and whether Spotify/Apple Music are connected). Use the returned _id to recognize your own songs/contacts.",
|
|
36578
|
-
annotations: toolAnnotations({ title: "Get my Vibo profile", readOnly: true })
|
|
36737
|
+
annotations: toolAnnotations({ title: "Get my Vibo profile", readOnly: true }),
|
|
36738
|
+
inputSchema: { view: viewArg() }
|
|
36579
36739
|
},
|
|
36580
|
-
async () => {
|
|
36740
|
+
async ({ view }) => {
|
|
36581
36741
|
const data = await client2.gql(GET_ME);
|
|
36582
|
-
return
|
|
36742
|
+
return viewResponse(view, data.me);
|
|
36583
36743
|
}
|
|
36584
36744
|
);
|
|
36585
36745
|
server.registerTool(
|
|
@@ -36745,15 +36905,6 @@ function registerSectionTools(server, client2) {
|
|
|
36745
36905
|
);
|
|
36746
36906
|
}
|
|
36747
36907
|
|
|
36748
|
-
// src/view.ts
|
|
36749
|
-
var VIBO_VIEWS = ["compact", "full"];
|
|
36750
|
-
var NOTE = `compact strips image/avatar URLs from the response; "full" returns Vibo's payload untouched. No field projection: this server has no verified record of which Vibo fields matter, and inventing one would risk dropping a field a caller needs.`;
|
|
36751
|
-
var viewArg = () => viewParam(VIBO_VIEWS, { note: NOTE });
|
|
36752
|
-
function viewResponse(view, data) {
|
|
36753
|
-
const rung = resolveView(view, VIBO_VIEWS);
|
|
36754
|
-
return minifiedResult(rung === "compact" ? stripMediaUrls(data) : data);
|
|
36755
|
-
}
|
|
36756
|
-
|
|
36757
36908
|
// src/song-search.ts
|
|
36758
36909
|
var NON_ORIGINAL_MARKERS = [
|
|
36759
36910
|
"karaoke",
|
|
@@ -36896,11 +37047,12 @@ function registerSongTools(server, client2) {
|
|
|
36896
37047
|
isFlagged: external_exports.boolean().optional().describe("Filter to do-not-play / flagged songs."),
|
|
36897
37048
|
sortField: external_exports.enum(["likesCount", "createdAt", "title"]).optional(),
|
|
36898
37049
|
sortDirection: external_exports.enum(["asc", "desc"]).optional(),
|
|
37050
|
+
view: viewArg(),
|
|
36899
37051
|
limit: limitSchema,
|
|
36900
37052
|
skip: skipSchema
|
|
36901
37053
|
}
|
|
36902
37054
|
},
|
|
36903
|
-
async ({ eventId, sectionId, q, isMustPlay, isFlagged, sortField, sortDirection, limit, skip }) => {
|
|
37055
|
+
async ({ eventId, sectionId, q, isMustPlay, isFlagged, sortField, sortDirection, limit, skip, view }) => {
|
|
36904
37056
|
const filter = {};
|
|
36905
37057
|
if (q !== void 0) filter.q = q;
|
|
36906
37058
|
if (isMustPlay !== void 0) filter.isMustPlay = isMustPlay;
|
|
@@ -36913,7 +37065,7 @@ function registerSongTools(server, client2) {
|
|
|
36913
37065
|
...sortField ? { sort: { field: sortField, direction: sortDirection ?? "desc" } } : {}
|
|
36914
37066
|
};
|
|
36915
37067
|
const data = await client2.gql(GET_SECTION_SONGS, variables);
|
|
36916
|
-
return
|
|
37068
|
+
return viewResponse(view, data.getSectionSongs);
|
|
36917
37069
|
}
|
|
36918
37070
|
);
|
|
36919
37071
|
server.registerTool(
|
|
@@ -37100,15 +37252,16 @@ function registerNotificationTools(server, client2) {
|
|
|
37100
37252
|
description: "List your Vibo notifications (song additions, comments, DJ updates, etc.) with read state and linked event/section ids.",
|
|
37101
37253
|
annotations: toolAnnotations({ title: "List Vibo notifications", readOnly: true }),
|
|
37102
37254
|
inputSchema: {
|
|
37255
|
+
view: viewArg(),
|
|
37103
37256
|
limit: limitSchema,
|
|
37104
37257
|
skip: skipSchema
|
|
37105
37258
|
}
|
|
37106
37259
|
},
|
|
37107
|
-
async ({ limit, skip }) => {
|
|
37260
|
+
async ({ limit, skip, view }) => {
|
|
37108
37261
|
const data = await client2.gql(GET_NOTIFICATIONS, {
|
|
37109
37262
|
pagination: pagination(limit, skip)
|
|
37110
37263
|
});
|
|
37111
|
-
return
|
|
37264
|
+
return viewResponse(view, data.getNotifications);
|
|
37112
37265
|
}
|
|
37113
37266
|
);
|
|
37114
37267
|
server.registerTool(
|
|
@@ -37497,18 +37650,19 @@ function registerIdeasTools(server, client2) {
|
|
|
37497
37650
|
eventId: external_exports.string().describe("Event id."),
|
|
37498
37651
|
sectionId: external_exports.string().describe("Section id (from vibo_list_sections)."),
|
|
37499
37652
|
songIdeasId: external_exports.string().describe("The _id from vibo_list_section_song_ideas."),
|
|
37653
|
+
view: viewArg(),
|
|
37500
37654
|
limit: limitSchema,
|
|
37501
37655
|
skip: skipSchema
|
|
37502
37656
|
}
|
|
37503
37657
|
},
|
|
37504
|
-
async ({ eventId, sectionId, songIdeasId, limit, skip }) => {
|
|
37658
|
+
async ({ eventId, sectionId, songIdeasId, limit, skip, view }) => {
|
|
37505
37659
|
const data = await client2.gql(LIST_SONG_IDEAS_SONGS, {
|
|
37506
37660
|
eventId,
|
|
37507
37661
|
sectionId,
|
|
37508
37662
|
songIdeasId,
|
|
37509
37663
|
pagination: pagination(limit, skip)
|
|
37510
37664
|
});
|
|
37511
|
-
return
|
|
37665
|
+
return viewResponse(view, data.getEventSectionSongIdeasSongs);
|
|
37512
37666
|
}
|
|
37513
37667
|
);
|
|
37514
37668
|
}
|
|
@@ -37559,11 +37713,12 @@ function registerCollaborationTools(server, client2) {
|
|
|
37559
37713
|
inputSchema: {
|
|
37560
37714
|
eventId: external_exports.string().describe("Event id."),
|
|
37561
37715
|
usersType: external_exports.enum(["host", "guest"]).optional().describe("Filter to only hosts or only guests."),
|
|
37716
|
+
view: viewArg(),
|
|
37562
37717
|
limit: limitSchema.describe("Max items to return (default 20). Applies per group when usersType is omitted."),
|
|
37563
37718
|
skip: skipSchema
|
|
37564
37719
|
}
|
|
37565
37720
|
},
|
|
37566
|
-
async ({ eventId, usersType, limit, skip }) => {
|
|
37721
|
+
async ({ eventId, usersType, limit, skip, view }) => {
|
|
37567
37722
|
const page = pagination(limit, skip);
|
|
37568
37723
|
if (usersType) {
|
|
37569
37724
|
const data = await client2.gql(LIST_EVENT_USERS, {
|
|
@@ -37571,13 +37726,13 @@ function registerCollaborationTools(server, client2) {
|
|
|
37571
37726
|
usersType,
|
|
37572
37727
|
pagination: page
|
|
37573
37728
|
});
|
|
37574
|
-
return
|
|
37729
|
+
return viewResponse(view, { ...data.eventUsers, usersType });
|
|
37575
37730
|
}
|
|
37576
37731
|
const [hosts, guests] = await Promise.all([
|
|
37577
37732
|
client2.gql(LIST_EVENT_USERS, { eventId, usersType: "host", pagination: page }),
|
|
37578
37733
|
client2.gql(LIST_EVENT_USERS, { eventId, usersType: "guest", pagination: page })
|
|
37579
37734
|
]);
|
|
37580
|
-
return
|
|
37735
|
+
return viewResponse(view, {
|
|
37581
37736
|
hosts: hosts.eventUsers.users,
|
|
37582
37737
|
guests: guests.eventUsers.users,
|
|
37583
37738
|
hostsCount: hosts.eventUsers.totalCount,
|
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { minifiedResult, schemaConfirm, toolAnnotations } from '@chrischall/mcp-utils';
|
|
3
3
|
import { LIST_EVENT_USERS, INVITE_USERS, CHANGE_USER_ROLE, REMOVE_USER } from '../gql.js';
|
|
4
4
|
import { limitSchema, skipSchema, pagination, previewResult } from './shared.js';
|
|
5
|
+
import { viewArg, viewResponse } from '../view.js';
|
|
5
6
|
export function registerCollaborationTools(server, client) {
|
|
6
7
|
server.registerTool('vibo_list_event_users', {
|
|
7
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,10 +10,11 @@ export function registerCollaborationTools(server, client) {
|
|
|
9
10
|
inputSchema: {
|
|
10
11
|
eventId: z.string().describe('Event id.'),
|
|
11
12
|
usersType: z.enum(['host', 'guest']).optional().describe('Filter to only hosts or only guests.'),
|
|
13
|
+
view: viewArg(),
|
|
12
14
|
limit: limitSchema.describe('Max items to return (default 20). Applies per group when usersType is omitted.'),
|
|
13
15
|
skip: skipSchema,
|
|
14
16
|
},
|
|
15
|
-
}, async ({ eventId, usersType, limit, skip }) => {
|
|
17
|
+
}, async ({ eventId, usersType, limit, skip, view }) => {
|
|
16
18
|
const page = pagination(limit, skip);
|
|
17
19
|
if (usersType) {
|
|
18
20
|
const data = await client.gql(LIST_EVENT_USERS, {
|
|
@@ -20,7 +22,7 @@ export function registerCollaborationTools(server, client) {
|
|
|
20
22
|
usersType,
|
|
21
23
|
pagination: page,
|
|
22
24
|
});
|
|
23
|
-
return
|
|
25
|
+
return viewResponse(view, { ...data.eventUsers, usersType });
|
|
24
26
|
}
|
|
25
27
|
// The API returns nothing unless usersType is set, so fetch both groups
|
|
26
28
|
// and merge for the intuitive "everyone on the event" listing.
|
|
@@ -28,7 +30,10 @@ export function registerCollaborationTools(server, client) {
|
|
|
28
30
|
client.gql(LIST_EVENT_USERS, { eventId, usersType: 'host', pagination: page }),
|
|
29
31
|
client.gql(LIST_EVENT_USERS, { eventId, usersType: 'guest', pagination: page }),
|
|
30
32
|
]);
|
|
31
|
-
|
|
33
|
+
// BOTH exits go through the rung. The merged branch is the one that runs
|
|
34
|
+
// when `usersType` is omitted — the default call — so honouring `view` on
|
|
35
|
+
// only the filtered branch would leave the common path paying full price.
|
|
36
|
+
return viewResponse(view, {
|
|
32
37
|
hosts: hosts.eventUsers.users,
|
|
33
38
|
guests: guests.eventUsers.users,
|
|
34
39
|
hostsCount: hosts.eventUsers.totalCount,
|
package/dist/tools/ideas.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
|
|
3
3
|
import { LIST_SECTION_SONG_IDEAS, LIST_SONG_IDEAS_SONGS } from '../gql.js';
|
|
4
4
|
import { limitSchema, skipSchema, pagination } from './shared.js';
|
|
5
|
+
import { viewArg, viewResponse } from '../view.js';
|
|
5
6
|
export function registerIdeasTools(server, client) {
|
|
6
7
|
server.registerTool('vibo_list_section_song_ideas', {
|
|
7
8
|
description: "List the DJ's suggested song-idea collections for a section (each with a title, songsCount and _id). Use a song-ideas _id with vibo_list_song_ideas_songs to see the suggested songs, then add the ones you like with vibo_add_song_to_section.",
|
|
@@ -27,16 +28,17 @@ export function registerIdeasTools(server, client) {
|
|
|
27
28
|
eventId: z.string().describe('Event id.'),
|
|
28
29
|
sectionId: z.string().describe('Section id (from vibo_list_sections).'),
|
|
29
30
|
songIdeasId: z.string().describe('The _id from vibo_list_section_song_ideas.'),
|
|
31
|
+
view: viewArg(),
|
|
30
32
|
limit: limitSchema,
|
|
31
33
|
skip: skipSchema,
|
|
32
34
|
},
|
|
33
|
-
}, async ({ eventId, sectionId, songIdeasId, limit, skip }) => {
|
|
35
|
+
}, async ({ eventId, sectionId, songIdeasId, limit, skip, view }) => {
|
|
34
36
|
const data = await client.gql(LIST_SONG_IDEAS_SONGS, {
|
|
35
37
|
eventId,
|
|
36
38
|
sectionId,
|
|
37
39
|
songIdeasId,
|
|
38
40
|
pagination: pagination(limit, skip),
|
|
39
41
|
});
|
|
40
|
-
return
|
|
42
|
+
return viewResponse(view, data.getEventSectionSongIdeasSongs);
|
|
41
43
|
});
|
|
42
44
|
}
|
|
@@ -2,19 +2,21 @@ import { z } from 'zod';
|
|
|
2
2
|
import { McpToolError, minifiedResult, schemaConfirm, toolAnnotations } from '@chrischall/mcp-utils';
|
|
3
3
|
import { GET_NOTIFICATIONS, GET_NOTIFICATIONS_COUNT, MARK_AS_READ } from '../gql.js';
|
|
4
4
|
import { limitSchema, skipSchema, pagination, previewResult } from './shared.js';
|
|
5
|
+
import { viewArg, viewResponse } from '../view.js';
|
|
5
6
|
export function registerNotificationTools(server, client) {
|
|
6
7
|
server.registerTool('vibo_list_notifications', {
|
|
7
8
|
description: 'List your Vibo notifications (song additions, comments, DJ updates, etc.) with read state and linked event/section ids.',
|
|
8
9
|
annotations: toolAnnotations({ title: 'List Vibo notifications', readOnly: true }),
|
|
9
10
|
inputSchema: {
|
|
11
|
+
view: viewArg(),
|
|
10
12
|
limit: limitSchema,
|
|
11
13
|
skip: skipSchema,
|
|
12
14
|
},
|
|
13
|
-
}, async ({ limit, skip }) => {
|
|
15
|
+
}, async ({ limit, skip, view }) => {
|
|
14
16
|
const data = await client.gql(GET_NOTIFICATIONS, {
|
|
15
17
|
pagination: pagination(limit, skip),
|
|
16
18
|
});
|
|
17
|
-
return
|
|
19
|
+
return viewResponse(view, data.getNotifications);
|
|
18
20
|
});
|
|
19
21
|
server.registerTool('vibo_get_notifications_count', {
|
|
20
22
|
description: 'Get the count of unread Vibo notifications.',
|
package/dist/tools/profile.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
|
|
2
2
|
import { GET_ME } from '../gql.js';
|
|
3
|
+
import { viewArg, viewResponse } from '../view.js';
|
|
3
4
|
export function registerProfileTools(server, client) {
|
|
4
5
|
server.registerTool('vibo_get_me', {
|
|
5
6
|
description: "Get the signed-in Vibo user's profile (id, name, email, phone, locale, and whether Spotify/Apple Music are connected). Use the returned _id to recognize your own songs/contacts.",
|
|
6
7
|
annotations: toolAnnotations({ title: 'Get my Vibo profile', readOnly: true }),
|
|
7
|
-
|
|
8
|
+
inputSchema: { view: viewArg() },
|
|
9
|
+
}, async ({ view }) => {
|
|
8
10
|
const data = await client.gql(GET_ME);
|
|
9
|
-
return
|
|
11
|
+
return viewResponse(view, data.me);
|
|
10
12
|
});
|
|
11
13
|
server.registerTool('vibo_healthcheck', {
|
|
12
14
|
description: 'Verify connectivity and authentication to the Vibo API by fetching the current user. Returns ok:true with your account id when credentials work.',
|
package/dist/tools/songs.js
CHANGED
|
@@ -16,10 +16,11 @@ export function registerSongTools(server, client) {
|
|
|
16
16
|
isFlagged: z.boolean().optional().describe('Filter to do-not-play / flagged songs.'),
|
|
17
17
|
sortField: z.enum(['likesCount', 'createdAt', 'title']).optional(),
|
|
18
18
|
sortDirection: z.enum(['asc', 'desc']).optional(),
|
|
19
|
+
view: viewArg(),
|
|
19
20
|
limit: limitSchema,
|
|
20
21
|
skip: skipSchema,
|
|
21
22
|
},
|
|
22
|
-
}, async ({ eventId, sectionId, q, isMustPlay, isFlagged, sortField, sortDirection, limit, skip }) => {
|
|
23
|
+
}, async ({ eventId, sectionId, q, isMustPlay, isFlagged, sortField, sortDirection, limit, skip, view }) => {
|
|
23
24
|
const filter = {};
|
|
24
25
|
if (q !== undefined)
|
|
25
26
|
filter.q = q;
|
|
@@ -35,7 +36,7 @@ export function registerSongTools(server, client) {
|
|
|
35
36
|
...(sortField ? { sort: { field: sortField, direction: sortDirection ?? 'desc' } } : {}),
|
|
36
37
|
};
|
|
37
38
|
const data = await client.gql(GET_SECTION_SONGS, variables);
|
|
38
|
-
return
|
|
39
|
+
return viewResponse(view, data.getSectionSongs);
|
|
39
40
|
});
|
|
40
41
|
server.registerTool('vibo_search_songs', {
|
|
41
42
|
description: 'Search for songs to add to a section. ALWAYS query as "<Artist> - <Title>" with a ' +
|
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.8.1'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibo-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"mcpName": "io.github.chrischall/vibo-mcp",
|
|
5
5
|
"description": "Vibo (vibodj.com) MCP server for Claude — host/couple event music planning & management. Developed and maintained by AI (Claude Code).",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -46,17 +46,17 @@
|
|
|
46
46
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@chrischall/mcp-utils": "^0.
|
|
49
|
+
"@chrischall/mcp-utils": "^0.26.1",
|
|
50
50
|
"@fetchproxy/bootstrap": "^2.2.0",
|
|
51
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
52
52
|
"dotenv": "^17.4.0",
|
|
53
|
-
"zod": "^4.4
|
|
53
|
+
"zod": "^4.5.4"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/node": "^26.0.0",
|
|
57
|
-
"@vitest/coverage-v8": "^
|
|
57
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
58
58
|
"esbuild": "^0.28.0",
|
|
59
59
|
"typescript": "^7.0.2",
|
|
60
|
-
"vitest": "^
|
|
60
|
+
"vitest": "^5.0.0"
|
|
61
61
|
}
|
|
62
62
|
}
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/vibo-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.8.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "vibo-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.8.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
package/skills/vibo-mcp/SKILL.md
CHANGED
|
@@ -87,63 +87,58 @@ you get a dry-run preview of exactly what would be sent.
|
|
|
87
87
|
|
|
88
88
|
## Response shape (`view`)
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
**Six of this server's 39 tools take `view: "compact" | "full"`**, and on every
|
|
91
|
+
one of them **`compact` is the DEFAULT**. You get the slim rung without asking.
|
|
92
|
+
|
|
93
|
+
They are exactly the six reads whose GraphQL document asks Vibo for media:
|
|
94
|
+
|
|
95
|
+
| Tool | What compact drops |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| `vibo_search_songs` | `thumbnails { s180x180 original }` per track |
|
|
98
|
+
| `vibo_get_section_songs` | `thumbnails { s180x180 original }` per song |
|
|
99
|
+
| `vibo_list_song_ideas_songs` | `thumbnails { s180x180 original }` per song |
|
|
100
|
+
| `vibo_list_event_users` | `imageUrl` per person (both exits — merged and filtered) |
|
|
101
|
+
| `vibo_list_notifications` | `imageUrl` per notification |
|
|
102
|
+
| `vibo_get_me` | `imageUrl` |
|
|
103
|
+
|
|
104
|
+
That correspondence is not a coincidence to be maintained by hand: a test in
|
|
105
|
+
`tests/view.test.ts` counts the media selections in `src/gql.ts` and asserts
|
|
106
|
+
the roster of `view`-taking tools matches. Add a seventh selection and it fails.
|
|
93
107
|
|
|
94
108
|
**Compact here is media stripping, not a field projection.** `src/view.ts`
|
|
95
109
|
writes no field list, because this repo holds no captured Vibo payload to
|
|
96
110
|
derive one from honestly; instead it removes keys whose value is a picture,
|
|
97
111
|
which is subtractive and so cannot drop a field nobody knew about.
|
|
98
112
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
What compact does **not** touch on that tool is the point of the tool:
|
|
107
|
-
`songUrl`, the `links` block (`spotify` / `youtube` / `appleMusic`) and the
|
|
108
|
-
`quality` verdict all survive. A streaming link is this server's product, not
|
|
109
|
-
decoration — `vibo_add_song_to_section` cannot add anything without `songUrl`,
|
|
110
|
-
and the quality verdict is computed from how many services carry a track. A
|
|
111
|
-
`null` link survives too: an absent key and a null one are the same to
|
|
112
|
-
`JSON.parse` but not to a reader deciding whether a service was checked.
|
|
113
|
+
What compact does **not** touch is the point of these tools: `songUrl`, the
|
|
114
|
+
`links` block (`spotify` / `youtube` / `appleMusic`) and the `quality` verdict
|
|
115
|
+
all survive. A streaming link is this server's product, not decoration —
|
|
116
|
+
`vibo_add_song_to_section` cannot add anything without `songUrl`, and the
|
|
117
|
+
quality verdict is computed from how many services carry a track. A `null` link
|
|
118
|
+
survives too: an absent key and a null one are the same to `JSON.parse` but not
|
|
119
|
+
to a reader deciding whether a service was checked.
|
|
113
120
|
|
|
114
121
|
`view: "full"` returns Vibo's payload untouched, cover art included. There is
|
|
115
122
|
deliberately **no `raw` rung**: nothing here re-serialises or normalises the
|
|
116
123
|
GraphQL response, so `full` already IS the upstream payload and a third value
|
|
117
124
|
would silently alias one that exists.
|
|
118
125
|
|
|
119
|
-
### The other
|
|
126
|
+
### The other 33 tools have no `view`
|
|
120
127
|
|
|
121
128
|
- **The 24 mutating tools** (every confirm-gated write, plus
|
|
122
129
|
`vibo_capture_session`) answer with a dry-run preview or a receipt — an id,
|
|
123
130
|
a count, a status. Nothing in a receipt is decoration, and slimming one is
|
|
124
131
|
how you lose the field that says what actually happened.
|
|
125
|
-
- **`vibo_healthcheck`** answers with a connectivity/auth diagnostic
|
|
126
|
-
same
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
| Tool | What arrives per record |
|
|
136
|
-
| --- | --- |
|
|
137
|
-
| `vibo_get_section_songs` | `thumbnails { s180x180 original }` per song |
|
|
138
|
-
| `vibo_list_song_ideas_songs` | `thumbnails { s180x180 original }` per song |
|
|
139
|
-
| `vibo_list_event_users` | `imageUrl` per person |
|
|
140
|
-
| `vibo_list_notifications` | `imageUrl` per notification |
|
|
141
|
-
| `vibo_get_me` | `imageUrl` |
|
|
142
|
-
|
|
143
|
-
`vibo_get_section_songs` is the one to watch: it returns the same catalog's
|
|
144
|
-
song records that `vibo_search_songs` returns, carrying the same per-track
|
|
145
|
-
cover art — but with no rung to drop it. Page it with `limit` rather than
|
|
146
|
-
reaching for a `view` that is not there.
|
|
132
|
+
- **`vibo_healthcheck`** answers with a connectivity/auth diagnostic. It runs
|
|
133
|
+
the same `GET_ME` document as `vibo_get_me` and still takes no rung, because
|
|
134
|
+
it never returns Vibo's user object — it builds `{ok, userId, email}` here.
|
|
135
|
+
A `view` on it would be a parameter that changes nothing.
|
|
136
|
+
- **The 8 remaining reads hand back Vibo's GraphQL payload as it arrived**, and
|
|
137
|
+
their documents select no media, so there is nothing for a rung to remove.
|
|
138
|
+
|
|
139
|
+
A `view` passed to a tool that does not declare one is dropped by zod without a
|
|
140
|
+
warning, so a successful call is never evidence the rung was honoured. Check
|
|
141
|
+
the table above rather than assuming.
|
|
147
142
|
|
|
148
143
|
## Typical flow
|
|
149
144
|
|