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.
@@ -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.0"
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.0",
18
+ "version": "0.6.1",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "setlist-mcp",
3
3
  "displayName": "setlist.fm",
4
- "version": "0.6.0",
4
+ "version": "0.6.1",
5
5
  "description": "MCP server for setlist.fm — search concert setlists, artists, venues, and tours via the setlist.fm API",
6
6
  "author": {
7
7
  "name": "Chris Hall",
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 sleep2 = opts.sleep ?? defaultSleep;
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 sleep2(retry.delayMs);
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.0";
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
- if (this.hostHandle) {
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
- if (this.hostHandle) {
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
- if (this.hostHandle) {
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
- if (this.hostHandle) {
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
- if (this.hostHandle) {
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
- if (this.hostHandle) {
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
- if (this.hostHandle) {
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: ["www.setlist.fm"],
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 sleep2 = deps.sleep ?? defaultSleep2;
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 sleep2(wait);
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 this.api.fetchHtml("GET", ajaxPath, {
40197
- headers: {
40198
- Cookie: cookie,
40199
- "Wicket-Ajax": "true",
40200
- "Wicket-Ajax-BaseURL": baseUrl,
40201
- "X-Requested-With": "XMLHttpRequest",
40202
- Accept: "text/xml, text/javascript, application/xml, text/html, */*"
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: ['www.setlist.fm'],
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.0'; // x-release-please-version
6
+ export const VERSION = '0.6.1'; // x-release-please-version
@@ -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.0",
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.0",
47
- "@fetchproxy/server": "^1.3.0",
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.0",
9
+ "version": "0.6.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "setlist-mcp",
14
- "version": "0.6.0",
14
+ "version": "0.6.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },