setlist-mcp 0.6.0 → 0.6.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 +128 -53
- package/dist/fetchproxy-cookie.js +6 -1
- package/dist/version.js +1 -1
- package/dist/web-client.js +25 -4
- package/package.json +3 -3
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for setlist.fm — concert setlists, artists, venues, and tours via the setlist.fm API",
|
|
10
|
-
"version": "0.6.
|
|
10
|
+
"version": "0.6.1"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "setlist.fm",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "MCP server for setlist.fm — search concert setlists, artists, venues, and tours via natural language",
|
|
18
|
-
"version": "0.6.
|
|
18
|
+
"version": "0.6.1",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/dist/bundle.js
CHANGED
|
@@ -32208,7 +32208,7 @@ function createApiClient(opts) {
|
|
|
32208
32208
|
const retry = opts.retry ?? DEFAULT_RETRY;
|
|
32209
32209
|
const service = opts.serviceName ?? hostOf(opts.baseUrl);
|
|
32210
32210
|
const doFetch = opts.fetchImpl ?? fetch;
|
|
32211
|
-
const
|
|
32211
|
+
const sleep3 = opts.sleep ?? defaultSleep;
|
|
32212
32212
|
const unauthorized = () => opts.onUnauthorized ? opts.onUnauthorized() : new UnauthorizedError(service);
|
|
32213
32213
|
const rateLimited = () => opts.onRateLimited ? opts.onRateLimited() : new RateLimitedError(service);
|
|
32214
32214
|
const timeoutMs = opts.timeout;
|
|
@@ -32248,7 +32248,7 @@ function createApiClient(opts) {
|
|
|
32248
32248
|
const res = await once();
|
|
32249
32249
|
if (res.status === 429 && attempt < retry.count) {
|
|
32250
32250
|
attempt += 1;
|
|
32251
|
-
await
|
|
32251
|
+
await sleep3(retry.delayMs);
|
|
32252
32252
|
continue;
|
|
32253
32253
|
}
|
|
32254
32254
|
return res;
|
|
@@ -32430,7 +32430,7 @@ var VERSION;
|
|
|
32430
32430
|
var init_version = __esm({
|
|
32431
32431
|
"src/version.ts"() {
|
|
32432
32432
|
"use strict";
|
|
32433
|
-
VERSION = "0.6.
|
|
32433
|
+
VERSION = "0.6.1";
|
|
32434
32434
|
}
|
|
32435
32435
|
});
|
|
32436
32436
|
|
|
@@ -37260,6 +37260,49 @@ var init_session = __esm({
|
|
|
37260
37260
|
}
|
|
37261
37261
|
});
|
|
37262
37262
|
|
|
37263
|
+
// node_modules/@fetchproxy/server/dist/session-ready.js
|
|
37264
|
+
async function awaitSessionReady(ready, opts) {
|
|
37265
|
+
const ms = opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS;
|
|
37266
|
+
if (ms <= 0)
|
|
37267
|
+
return ready;
|
|
37268
|
+
let timer;
|
|
37269
|
+
const timeout = new Promise((_, reject) => {
|
|
37270
|
+
timer = setTimeout(() => {
|
|
37271
|
+
reject(new FetchproxySessionNotReadyError({ mcpId: opts.mcpId, pairCode: opts.pendingPairCode() }));
|
|
37272
|
+
}, ms);
|
|
37273
|
+
timer.unref?.();
|
|
37274
|
+
});
|
|
37275
|
+
try {
|
|
37276
|
+
return await Promise.race([ready, timeout]);
|
|
37277
|
+
} finally {
|
|
37278
|
+
if (timer)
|
|
37279
|
+
clearTimeout(timer);
|
|
37280
|
+
}
|
|
37281
|
+
}
|
|
37282
|
+
var SESSION_READY_TIMEOUT_MS, FetchproxySessionNotReadyError;
|
|
37283
|
+
var init_session_ready = __esm({
|
|
37284
|
+
"node_modules/@fetchproxy/server/dist/session-ready.js"() {
|
|
37285
|
+
SESSION_READY_TIMEOUT_MS = 3e4;
|
|
37286
|
+
FetchproxySessionNotReadyError = class extends Error {
|
|
37287
|
+
reason;
|
|
37288
|
+
pairCode;
|
|
37289
|
+
mcpId;
|
|
37290
|
+
hint;
|
|
37291
|
+
constructor(info) {
|
|
37292
|
+
const pairing = info.pairCode !== null && info.pairCode !== "";
|
|
37293
|
+
const hint = pairing ? `Open the Transporter extension popup and approve pair code ${info.pairCode} for "${info.mcpId}", then retry.` : `The extension is connected but hasn't confirmed a session for "${info.mcpId}" \u2014 sign in to the target site in that browser (and approve the requested scope if it changed), then retry.`;
|
|
37294
|
+
super(`fetchproxy: ${pairing ? "pairing not yet approved" : "no confirmed browser session"} for "${info.mcpId}". ${hint}`);
|
|
37295
|
+
this.name = "FetchproxySessionNotReadyError";
|
|
37296
|
+
this.reason = pairing ? "pair-required" : "not-ready";
|
|
37297
|
+
this.pairCode = pairing ? info.pairCode : null;
|
|
37298
|
+
this.mcpId = info.mcpId;
|
|
37299
|
+
this.hint = hint;
|
|
37300
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
37301
|
+
}
|
|
37302
|
+
};
|
|
37303
|
+
}
|
|
37304
|
+
});
|
|
37305
|
+
|
|
37263
37306
|
// node_modules/@fetchproxy/server/dist/host.js
|
|
37264
37307
|
async function startHost(opts) {
|
|
37265
37308
|
const wss = new import_websocket_server.default({
|
|
@@ -37454,7 +37497,10 @@ async function startHost(opts) {
|
|
|
37454
37497
|
});
|
|
37455
37498
|
}),
|
|
37456
37499
|
sendOwnInner: async (inner) => {
|
|
37457
|
-
const session = await ownSessionReady
|
|
37500
|
+
const session = await awaitSessionReady(ownSessionReady, {
|
|
37501
|
+
mcpId: opts.ownMcpId,
|
|
37502
|
+
pendingPairCode: () => ownPendingPairCode
|
|
37503
|
+
});
|
|
37458
37504
|
if (!extensionWs)
|
|
37459
37505
|
throw new Error("host: no extension connected");
|
|
37460
37506
|
const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), inner);
|
|
@@ -37479,6 +37525,7 @@ var init_host = __esm({
|
|
|
37479
37525
|
init_dist2();
|
|
37480
37526
|
init_build_server_hello();
|
|
37481
37527
|
init_session();
|
|
37528
|
+
init_session_ready();
|
|
37482
37529
|
PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
|
|
37483
37530
|
enc2 = new TextEncoder();
|
|
37484
37531
|
}
|
|
@@ -37569,7 +37616,10 @@ async function startPeer(opts) {
|
|
|
37569
37616
|
ws,
|
|
37570
37617
|
session: sessionPromise,
|
|
37571
37618
|
sendInner: async (inner) => {
|
|
37572
|
-
await sessionPromise
|
|
37619
|
+
await awaitSessionReady(sessionPromise, {
|
|
37620
|
+
mcpId: opts.mcpId,
|
|
37621
|
+
pendingPairCode: () => pendingPairCode
|
|
37622
|
+
});
|
|
37573
37623
|
const s = session;
|
|
37574
37624
|
const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
|
|
37575
37625
|
ws.send(JSON.stringify(sealed));
|
|
@@ -37598,6 +37648,7 @@ var init_peer = __esm({
|
|
|
37598
37648
|
init_dist2();
|
|
37599
37649
|
init_build_server_hello();
|
|
37600
37650
|
init_session();
|
|
37651
|
+
init_session_ready();
|
|
37601
37652
|
enc3 = new TextEncoder();
|
|
37602
37653
|
}
|
|
37603
37654
|
});
|
|
@@ -38270,6 +38321,35 @@ var init_ws_server = __esm({
|
|
|
38270
38321
|
this.keepAliveTimer = null;
|
|
38271
38322
|
}
|
|
38272
38323
|
}
|
|
38324
|
+
/**
|
|
38325
|
+
* Send an inner request frame via whichever bridge handle is active. If the
|
|
38326
|
+
* send throws (e.g. `FetchproxySessionNotReadyError` — the session never
|
|
38327
|
+
* confirmed), the frame never reached the bridge, so no reply will arrive:
|
|
38328
|
+
* drop the just-registered pending resolver for this id (it lives in exactly
|
|
38329
|
+
* one of the op maps — request ids are unique) so it doesn't leak until the
|
|
38330
|
+
* server closes, then rethrow.
|
|
38331
|
+
*/
|
|
38332
|
+
async sendInnerFrame(inner) {
|
|
38333
|
+
try {
|
|
38334
|
+
if (this.hostHandle) {
|
|
38335
|
+
await this.hostHandle.sendOwnInner(inner);
|
|
38336
|
+
} else if (this.peerHandle) {
|
|
38337
|
+
await this.peerHandle.sendInner(inner);
|
|
38338
|
+
}
|
|
38339
|
+
} catch (err) {
|
|
38340
|
+
if ("id" in inner && typeof inner.id === "number") {
|
|
38341
|
+
const { id } = inner;
|
|
38342
|
+
this.pending.delete(id);
|
|
38343
|
+
this.pendingReadCookies.delete(id);
|
|
38344
|
+
this.pendingStorage.delete(id);
|
|
38345
|
+
this.pendingCapture.delete(id);
|
|
38346
|
+
this.pendingRedirect.delete(id);
|
|
38347
|
+
this.pendingDownload.delete(id);
|
|
38348
|
+
this.pendingIdb.delete(id);
|
|
38349
|
+
}
|
|
38350
|
+
throw err;
|
|
38351
|
+
}
|
|
38352
|
+
}
|
|
38273
38353
|
/**
|
|
38274
38354
|
* Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
|
|
38275
38355
|
* On timeout returns the `{ok:false, kind:'timeout'}` envelope —
|
|
@@ -38281,11 +38361,7 @@ var init_ws_server = __esm({
|
|
|
38281
38361
|
const pending = new Promise((resolve) => {
|
|
38282
38362
|
this.pending.set(id, resolve);
|
|
38283
38363
|
});
|
|
38284
|
-
|
|
38285
|
-
await this.hostHandle.sendOwnInner(inner);
|
|
38286
|
-
} else if (this.peerHandle) {
|
|
38287
|
-
await this.peerHandle.sendInner(inner);
|
|
38288
|
-
}
|
|
38364
|
+
await this.sendInnerFrame(inner);
|
|
38289
38365
|
const timeoutMs = this.opts.fetchTimeoutMs;
|
|
38290
38366
|
if (timeoutMs === void 0 || timeoutMs <= 0)
|
|
38291
38367
|
return pending;
|
|
@@ -38604,11 +38680,7 @@ var init_ws_server = __esm({
|
|
|
38604
38680
|
const pending = new Promise((resolve) => {
|
|
38605
38681
|
this.pendingReadCookies.set(id, resolve);
|
|
38606
38682
|
});
|
|
38607
|
-
|
|
38608
|
-
await this.hostHandle.sendOwnInner(inner);
|
|
38609
|
-
} else if (this.peerHandle) {
|
|
38610
|
-
await this.peerHandle.sendInner(inner);
|
|
38611
|
-
}
|
|
38683
|
+
await this.sendInnerFrame(inner);
|
|
38612
38684
|
const result = await pending;
|
|
38613
38685
|
if (!result.ok) {
|
|
38614
38686
|
throw new FetchproxyProtocolError(result.error);
|
|
@@ -38675,11 +38747,7 @@ var init_ws_server = __esm({
|
|
|
38675
38747
|
const pending = new Promise((resolve, reject) => {
|
|
38676
38748
|
this.pendingStorage.set(id, { resolve, reject });
|
|
38677
38749
|
});
|
|
38678
|
-
|
|
38679
|
-
await this.hostHandle.sendOwnInner(inner);
|
|
38680
|
-
} else if (this.peerHandle) {
|
|
38681
|
-
await this.peerHandle.sendInner(inner);
|
|
38682
|
-
}
|
|
38750
|
+
await this.sendInnerFrame(inner);
|
|
38683
38751
|
return pending;
|
|
38684
38752
|
}
|
|
38685
38753
|
/**
|
|
@@ -38784,11 +38852,7 @@ var init_ws_server = __esm({
|
|
|
38784
38852
|
const pending = new Promise((resolve, reject) => {
|
|
38785
38853
|
this.pendingCapture.set(id, { resolve, reject });
|
|
38786
38854
|
});
|
|
38787
|
-
|
|
38788
|
-
await this.hostHandle.sendOwnInner(inner);
|
|
38789
|
-
} else if (this.peerHandle) {
|
|
38790
|
-
await this.peerHandle.sendInner(inner);
|
|
38791
|
-
}
|
|
38855
|
+
await this.sendInnerFrame(inner);
|
|
38792
38856
|
return pending;
|
|
38793
38857
|
}
|
|
38794
38858
|
/**
|
|
@@ -38873,11 +38937,7 @@ var init_ws_server = __esm({
|
|
|
38873
38937
|
const pending = new Promise((resolve, reject) => {
|
|
38874
38938
|
this.pendingRedirect.set(id, { resolve, reject });
|
|
38875
38939
|
});
|
|
38876
|
-
|
|
38877
|
-
await this.hostHandle.sendOwnInner(inner);
|
|
38878
|
-
} else if (this.peerHandle) {
|
|
38879
|
-
await this.peerHandle.sendInner(inner);
|
|
38880
|
-
}
|
|
38940
|
+
await this.sendInnerFrame(inner);
|
|
38881
38941
|
return pending;
|
|
38882
38942
|
}
|
|
38883
38943
|
/**
|
|
@@ -38959,11 +39019,7 @@ var init_ws_server = __esm({
|
|
|
38959
39019
|
const pending = new Promise((resolve, reject) => {
|
|
38960
39020
|
this.pendingDownload.set(id, { resolve, reject });
|
|
38961
39021
|
});
|
|
38962
|
-
|
|
38963
|
-
await this.hostHandle.sendOwnInner(inner);
|
|
38964
|
-
} else if (this.peerHandle) {
|
|
38965
|
-
await this.peerHandle.sendInner(inner);
|
|
38966
|
-
}
|
|
39022
|
+
await this.sendInnerFrame(inner);
|
|
38967
39023
|
return pending;
|
|
38968
39024
|
}
|
|
38969
39025
|
/**
|
|
@@ -39011,11 +39067,7 @@ var init_ws_server = __esm({
|
|
|
39011
39067
|
const pending = new Promise((resolve, reject) => {
|
|
39012
39068
|
this.pendingIdb.set(id, { resolve, reject });
|
|
39013
39069
|
});
|
|
39014
|
-
|
|
39015
|
-
await this.hostHandle.sendOwnInner(inner);
|
|
39016
|
-
} else if (this.peerHandle) {
|
|
39017
|
-
await this.peerHandle.sendInner(inner);
|
|
39018
|
-
}
|
|
39070
|
+
await this.sendInnerFrame(inner);
|
|
39019
39071
|
return pending;
|
|
39020
39072
|
}
|
|
39021
39073
|
assertScopeSubset(requested, declared, label) {
|
|
@@ -39308,6 +39360,7 @@ var init_dist3 = __esm({
|
|
|
39308
39360
|
"node_modules/@fetchproxy/server/dist/index.js"() {
|
|
39309
39361
|
init_ws_server();
|
|
39310
39362
|
init_ws_server();
|
|
39363
|
+
init_session_ready();
|
|
39311
39364
|
init_error_kind();
|
|
39312
39365
|
init_classify_bridge_error();
|
|
39313
39366
|
init_bulk();
|
|
@@ -39519,7 +39572,8 @@ async function grabSessionCookie() {
|
|
|
39519
39572
|
bootstrap2({
|
|
39520
39573
|
serverName: "setlist-mcp",
|
|
39521
39574
|
version: VERSION,
|
|
39522
|
-
domains: ["
|
|
39575
|
+
domains: ["setlist.fm"],
|
|
39576
|
+
storageSubdomain: "www",
|
|
39523
39577
|
declare: { cookies: SESSION_COOKIE_KEYS, localStorage: [], sessionStorage: [], captureHeaders: [] }
|
|
39524
39578
|
}),
|
|
39525
39579
|
BOOTSTRAP_TIMEOUT_MS
|
|
@@ -40064,7 +40118,7 @@ async function resolveOne(req, c, tourFallback) {
|
|
|
40064
40118
|
var defaultSleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
40065
40119
|
async function resolveConcerts(concerts, deps = {}) {
|
|
40066
40120
|
const baseRequest = deps.request ?? ((m, p, o) => client.request(m, p, o));
|
|
40067
|
-
const
|
|
40121
|
+
const sleep3 = deps.sleep ?? defaultSleep2;
|
|
40068
40122
|
const now = deps.now ?? Date.now;
|
|
40069
40123
|
const paceMs = deps.paceMs ?? PACE_MS;
|
|
40070
40124
|
const budgetMs = deps.budgetMs ?? BUDGET_MS;
|
|
@@ -40072,7 +40126,7 @@ async function resolveConcerts(concerts, deps = {}) {
|
|
|
40072
40126
|
let lastCallAt = 0;
|
|
40073
40127
|
const req = async (method, path, opts) => {
|
|
40074
40128
|
const wait = paceMs - (now() - lastCallAt);
|
|
40075
|
-
if (wait > 0) await
|
|
40129
|
+
if (wait > 0) await sleep3(wait);
|
|
40076
40130
|
lastCallAt = now();
|
|
40077
40131
|
return baseRequest(method, path, opts);
|
|
40078
40132
|
};
|
|
@@ -40143,6 +40197,25 @@ import { dirname as dirname2, join as join3 } from "path";
|
|
|
40143
40197
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
40144
40198
|
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
40145
40199
|
await loadDotenvSafely({ path: join3(__dirname2, "..", ".env"), override: false });
|
|
40200
|
+
var RETRY_5XX = 3;
|
|
40201
|
+
var RETRY_DELAY_MS = 1200;
|
|
40202
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
40203
|
+
async function retryOn5xx(fn) {
|
|
40204
|
+
let lastErr;
|
|
40205
|
+
for (let attempt = 0; attempt <= RETRY_5XX; attempt++) {
|
|
40206
|
+
try {
|
|
40207
|
+
return await fn();
|
|
40208
|
+
} catch (err) {
|
|
40209
|
+
lastErr = err;
|
|
40210
|
+
if (attempt < RETRY_5XX && /\b50[0234]\b/.test(messageOf(err))) {
|
|
40211
|
+
await sleep2(RETRY_DELAY_MS);
|
|
40212
|
+
continue;
|
|
40213
|
+
}
|
|
40214
|
+
throw err;
|
|
40215
|
+
}
|
|
40216
|
+
}
|
|
40217
|
+
throw lastErr;
|
|
40218
|
+
}
|
|
40146
40219
|
var BASE_URL2 = "https://www.setlist.fm";
|
|
40147
40220
|
var SERVICE_NAME2 = "setlist.fm (web)";
|
|
40148
40221
|
var REQUEST_TIMEOUT_MS2 = 2e4;
|
|
@@ -40183,7 +40256,7 @@ var SetlistWebClient = class {
|
|
|
40183
40256
|
/** GET a page as HTML, authenticated. `path` is appended to the www base URL. */
|
|
40184
40257
|
async fetchPage(path) {
|
|
40185
40258
|
const cookie = await this.requireCookie();
|
|
40186
|
-
return this.api.fetchHtml("GET", path, { headers: { Cookie: cookie } });
|
|
40259
|
+
return retryOn5xx(() => this.api.fetchHtml("GET", path, { headers: { Cookie: cookie } }));
|
|
40187
40260
|
}
|
|
40188
40261
|
/**
|
|
40189
40262
|
* Replay an Apache Wicket AJAX behavior GET (e.g. the attendance toggle).
|
|
@@ -40193,15 +40266,17 @@ var SetlistWebClient = class {
|
|
|
40193
40266
|
*/
|
|
40194
40267
|
async wicketAjaxGet(ajaxPath, baseUrl) {
|
|
40195
40268
|
const cookie = await this.requireCookie();
|
|
40196
|
-
return
|
|
40197
|
-
|
|
40198
|
-
|
|
40199
|
-
|
|
40200
|
-
|
|
40201
|
-
|
|
40202
|
-
|
|
40203
|
-
|
|
40204
|
-
|
|
40269
|
+
return retryOn5xx(
|
|
40270
|
+
() => this.api.fetchHtml("GET", ajaxPath, {
|
|
40271
|
+
headers: {
|
|
40272
|
+
Cookie: cookie,
|
|
40273
|
+
"Wicket-Ajax": "true",
|
|
40274
|
+
"Wicket-Ajax-BaseURL": baseUrl,
|
|
40275
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
40276
|
+
Accept: "text/xml, text/javascript, application/xml, text/html, */*"
|
|
40277
|
+
}
|
|
40278
|
+
})
|
|
40279
|
+
);
|
|
40205
40280
|
}
|
|
40206
40281
|
};
|
|
40207
40282
|
var webClient = new SetlistWebClient();
|
|
@@ -37,10 +37,15 @@ export async function grabSessionCookie() {
|
|
|
37
37
|
catch {
|
|
38
38
|
return null; // bridge package unavailable (shouldn't happen — bundled)
|
|
39
39
|
}
|
|
40
|
+
// Declare the apex `setlist.fm` scope (so a re-render with no scope change
|
|
41
|
+
// never needs re-approval), but read cookies from the `www` subdomain — the
|
|
42
|
+
// session cookies are host-only on www.setlist.fm, and www is a subdomain of
|
|
43
|
+
// the approved apex, so chrome.cookies.get sees JSESSIONID without a re-pair.
|
|
40
44
|
const session = await withTimeout(bootstrap({
|
|
41
45
|
serverName: 'setlist-mcp',
|
|
42
46
|
version: VERSION,
|
|
43
|
-
domains: ['
|
|
47
|
+
domains: ['setlist.fm'],
|
|
48
|
+
storageSubdomain: 'www',
|
|
44
49
|
declare: { cookies: SESSION_COOKIE_KEYS, localStorage: [], sessionStorage: [], captureHeaders: [] },
|
|
45
50
|
}), BOOTSTRAP_TIMEOUT_MS);
|
|
46
51
|
const cookies = session.cookies ?? {};
|
package/dist/version.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
// release-please-config.json's `extra-files`), and `versionSyncTest` guards
|
|
4
4
|
// that it stays equal to package.json. Import VERSION wherever the version is
|
|
5
5
|
// needed rather than re-declaring it.
|
|
6
|
-
export const VERSION = '0.6.
|
|
6
|
+
export const VERSION = '0.6.1'; // x-release-please-version
|
package/dist/web-client.js
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
import { dirname, join } from 'path';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
|
-
import { loadDotenvSafely, readEnvVar, createApiClient } from '@chrischall/mcp-utils';
|
|
3
|
+
import { loadDotenvSafely, readEnvVar, createApiClient, messageOf } from '@chrischall/mcp-utils';
|
|
4
4
|
// Load .env for local dev (guarded; the mcpb bundle omits dotenv).
|
|
5
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
await loadDotenvSafely({ path: join(__dirname, '..', '.env'), override: false });
|
|
7
|
+
const RETRY_5XX = 3; // www.setlist.fm intermittently returns 500/502/503 from its gateway
|
|
8
|
+
const RETRY_DELAY_MS = 1200;
|
|
9
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
10
|
+
// Retry transient gateway errors (502/503/504); createApiClient only retries 429.
|
|
11
|
+
async function retryOn5xx(fn) {
|
|
12
|
+
let lastErr;
|
|
13
|
+
for (let attempt = 0; attempt <= RETRY_5XX; attempt++) {
|
|
14
|
+
try {
|
|
15
|
+
return await fn();
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
lastErr = err;
|
|
19
|
+
if (attempt < RETRY_5XX && /\b50[0234]\b/.test(messageOf(err))) {
|
|
20
|
+
await sleep(RETRY_DELAY_MS);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
throw lastErr;
|
|
27
|
+
}
|
|
7
28
|
const BASE_URL = 'https://www.setlist.fm';
|
|
8
29
|
const SERVICE_NAME = 'setlist.fm (web)';
|
|
9
30
|
const REQUEST_TIMEOUT_MS = 20_000;
|
|
@@ -54,7 +75,7 @@ export class SetlistWebClient {
|
|
|
54
75
|
/** GET a page as HTML, authenticated. `path` is appended to the www base URL. */
|
|
55
76
|
async fetchPage(path) {
|
|
56
77
|
const cookie = await this.requireCookie();
|
|
57
|
-
return this.api.fetchHtml('GET', path, { headers: { Cookie: cookie } });
|
|
78
|
+
return retryOn5xx(() => this.api.fetchHtml('GET', path, { headers: { Cookie: cookie } }));
|
|
58
79
|
}
|
|
59
80
|
/**
|
|
60
81
|
* Replay an Apache Wicket AJAX behavior GET (e.g. the attendance toggle).
|
|
@@ -64,7 +85,7 @@ export class SetlistWebClient {
|
|
|
64
85
|
*/
|
|
65
86
|
async wicketAjaxGet(ajaxPath, baseUrl) {
|
|
66
87
|
const cookie = await this.requireCookie();
|
|
67
|
-
return this.api.fetchHtml('GET', ajaxPath, {
|
|
88
|
+
return retryOn5xx(() => this.api.fetchHtml('GET', ajaxPath, {
|
|
68
89
|
headers: {
|
|
69
90
|
Cookie: cookie,
|
|
70
91
|
'Wicket-Ajax': 'true',
|
|
@@ -72,7 +93,7 @@ export class SetlistWebClient {
|
|
|
72
93
|
'X-Requested-With': 'XMLHttpRequest',
|
|
73
94
|
Accept: 'text/xml, text/javascript, application/xml, text/html, */*',
|
|
74
95
|
},
|
|
75
|
-
});
|
|
96
|
+
}));
|
|
76
97
|
}
|
|
77
98
|
}
|
|
78
99
|
/** Module-level singleton (deferred-config: missing session surfaces at request time). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "setlist-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"mcpName": "io.github.chrischall/setlist-mcp",
|
|
5
5
|
"description": "setlist.fm MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@chrischall/mcp-utils": "^0.6.0",
|
|
46
|
-
"@fetchproxy/bootstrap": "^1.3.
|
|
47
|
-
"@fetchproxy/server": "^1.3.
|
|
46
|
+
"@fetchproxy/bootstrap": "^1.3.1",
|
|
47
|
+
"@fetchproxy/server": "^1.3.1",
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
49
|
"dotenv": "^17.4.0",
|
|
50
50
|
"zod": "^4.4.2"
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/setlist-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.6.
|
|
9
|
+
"version": "0.6.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "setlist-mcp",
|
|
14
|
-
"version": "0.6.
|
|
14
|
+
"version": "0.6.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|