setlist-mcp 0.3.0 → 0.4.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.
@@ -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.3.0"
10
+ "version": "0.4.0"
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.3.0",
18
+ "version": "0.4.0",
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.3.0",
4
+ "version": "0.4.0",
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/SKILL.md CHANGED
@@ -72,7 +72,7 @@ All tools are read-only and prefixed `setlist_`.
72
72
  - **`setlist_get_artist_setlists`** — an artist's setlists (most recent first), by `mbid`, paginated via `p`.
73
73
 
74
74
  ### Setlists
75
- - **`setlist_search_setlists`** — search by any mix of artist, venue, city, country, tour, `date` (dd-MM-yyyy), or `year`.
75
+ - **`setlist_search_setlists`** — search by any mix of artist, venue, city, country, tour, `date` (ISO yyyy-MM-dd), or `year`.
76
76
  - **`setlist_get_setlist`** — a setlist (with full song list) by `setlistId`.
77
77
  - **`setlist_get_setlist_version`** — a specific historical version by `versionId`.
78
78
 
@@ -98,7 +98,7 @@ All tools are read-only and prefixed `setlist_`.
98
98
 
99
99
  - **"What did Radiohead play at their last show?"** → `setlist_search_artists` (Radiohead → mbid) → `setlist_get_artist_setlists` (latest) → `setlist_get_setlist` for the song list.
100
100
  - **"Setlists at Red Rocks in 2023"** → `setlist_search_venues` (Red Rocks → venueId) → `setlist_search_setlists` with `venueId` + `year: 2023`.
101
- - **"Phish on 2023-08-07"** → `setlist_search_setlists` with `artistName: "Phish"`, `date: "07-08-2023"` (note the dd-MM-yyyy format).
101
+ - **"Phish on 2023-08-07"** → `setlist_search_setlists` with `artistName: "Phish"`, `date: "2023-08-07"`.
102
102
 
103
103
  ## Attribution & API terms
104
104
 
@@ -117,6 +117,6 @@ setlist.fm's [API terms](https://www.setlist.fm/help/api-terms) bind anyone usin
117
117
  ## Notes
118
118
 
119
119
  - IDs chain: `search_*` tools return the `mbid` / `setlistId` / `venueId` / `geoId` you feed into the `get_*` tools.
120
- - `date` filters use **dd-MM-yyyy** (e.g. `07-08-2023` = 7 August 2023).
120
+ - **All dates are ISO `yyyy-MM-dd`** — both the `date`/`lastUpdated` inputs and every `eventDate` in the output. (The server translates to/from setlist.fm's native `dd-MM-yyyy` internally.)
121
121
  - Results are paginated; pass `p` (1-based) to page through large result sets.
122
122
  - setlist.fm rate-limits the standard tier (~2 req/sec); a 429 is retried once.
package/dist/bundle.js CHANGED
@@ -31001,6 +31001,22 @@ function textResult(data) {
31001
31001
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
31002
31002
  };
31003
31003
  }
31004
+ function deepMapStringField(value, field, map2) {
31005
+ if (Array.isArray(value)) {
31006
+ for (const item of value)
31007
+ deepMapStringField(item, field, map2);
31008
+ } else if (value !== null && typeof value === "object") {
31009
+ const obj = value;
31010
+ for (const key of Object.keys(obj)) {
31011
+ const v = obj[key];
31012
+ if (key === field && typeof v === "string")
31013
+ obj[key] = map2(v);
31014
+ else
31015
+ deepMapStringField(v, field, map2);
31016
+ }
31017
+ }
31018
+ return value;
31019
+ }
31004
31020
 
31005
31021
  // node_modules/@chrischall/mcp-utils/dist/errors/index.js
31006
31022
  var DEFAULT_ERROR_MESSAGE_MAX = 500;
@@ -31071,6 +31087,15 @@ var RateLimitedError = class extends Error {
31071
31087
  Object.setPrototypeOf(this, new.target.prototype);
31072
31088
  }
31073
31089
  };
31090
+ var RequestTimeoutError = class extends Error {
31091
+ timeoutMs;
31092
+ constructor(service, timeoutMs) {
31093
+ super(`Request to ${service} timed out after ${timeoutMs}ms.`);
31094
+ this.name = "RequestTimeoutError";
31095
+ this.timeoutMs = timeoutMs;
31096
+ Object.setPrototypeOf(this, new.target.prototype);
31097
+ }
31098
+ };
31074
31099
  function hostOf(baseUrl) {
31075
31100
  try {
31076
31101
  return new URL(baseUrl).host;
@@ -31086,6 +31111,18 @@ function createApiClient(opts) {
31086
31111
  const sleep = opts.sleep ?? defaultSleep;
31087
31112
  const unauthorized = () => opts.onUnauthorized ? opts.onUnauthorized() : new UnauthorizedError(service);
31088
31113
  const rateLimited = () => opts.onRateLimited ? opts.onRateLimited() : new RateLimitedError(service);
31114
+ const timeoutMs = opts.timeout;
31115
+ function withTimeout(run) {
31116
+ if (timeoutMs == null || timeoutMs <= 0)
31117
+ return run(void 0);
31118
+ const controller = new AbortController();
31119
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
31120
+ return run(controller.signal).catch((err) => {
31121
+ if (err instanceof Error && err.name === "AbortError")
31122
+ throw new RequestTimeoutError(service, timeoutMs);
31123
+ throw err;
31124
+ }).finally(() => clearTimeout(timer));
31125
+ }
31089
31126
  async function send(method, path, opt) {
31090
31127
  const isMultipart = opt.formData !== void 0;
31091
31128
  const hasJsonBody = !isMultipart && opt.body !== void 0;
@@ -31093,7 +31130,7 @@ function createApiClient(opts) {
31093
31130
  const query = opt.query ? buildQueryString(opt.query) : "";
31094
31131
  const url2 = `${base}${path}${query}`;
31095
31132
  const bodyInit = reqBody !== void 0 ? { body: reqBody } : {};
31096
- const fetchWith = (token) => doFetch(url2, {
31133
+ const fetchWith = (token, signal) => doFetch(url2, {
31097
31134
  method,
31098
31135
  headers: {
31099
31136
  Accept: "application/json",
@@ -31102,9 +31139,10 @@ function createApiClient(opts) {
31102
31139
  ...token ? { Authorization: `Bearer ${token}` } : {},
31103
31140
  ...opt.headers
31104
31141
  },
31142
+ ...signal ? { signal } : {},
31105
31143
  ...bodyInit
31106
31144
  });
31107
- const once = opts.tokenManager ? () => opts.tokenManager.withAuth(fetchWith) : async () => fetchWith(await opts.getToken?.() || void 0);
31145
+ const once = () => withTimeout((signal) => opts.tokenManager ? opts.tokenManager.withAuth((token) => fetchWith(token, signal)) : (async () => fetchWith(await opts.getToken?.() || void 0, signal))());
31108
31146
  let attempt = 0;
31109
31147
  for (; ; ) {
31110
31148
  const res = await once();
@@ -31171,6 +31209,29 @@ function formatApiError(status, method, path, errorText, opts = {}) {
31171
31209
  return safe.length > 0 ? `${head}: ${safe}` : head;
31172
31210
  }
31173
31211
 
31212
+ // node_modules/@chrischall/mcp-utils/dist/dates/index.js
31213
+ var ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
31214
+ var DMY_DATE = /^(\d{2})-(\d{2})-(\d{4})$/;
31215
+ var ISO_DATETIME = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?$/;
31216
+ function isoToDmy(date5) {
31217
+ const m = ISO_DATE.exec(date5.trim());
31218
+ return m ? `${m[3]}-${m[2]}-${m[1]}` : date5.trim();
31219
+ }
31220
+ function dmyToIso(date5) {
31221
+ const m = DMY_DATE.exec(date5.trim());
31222
+ return m ? `${m[3]}-${m[2]}-${m[1]}` : date5.trim();
31223
+ }
31224
+ function isoToCompactTimestamp(value) {
31225
+ const v = value.trim();
31226
+ const d = ISO_DATE.exec(v);
31227
+ if (d)
31228
+ return `${d[1]}${d[2]}${d[3]}000000`;
31229
+ const dt = ISO_DATETIME.exec(v);
31230
+ if (dt)
31231
+ return `${dt[1]}${dt[2]}${dt[3]}${dt[4]}${dt[5]}${dt[6] ?? "00"}`;
31232
+ return v;
31233
+ }
31234
+
31174
31235
  // node_modules/@chrischall/mcp-utils/dist/zod/index.js
31175
31236
  var PositiveInt = external_exports.number().int().positive();
31176
31237
  var NonNegInt = external_exports.number().int().nonnegative();
@@ -31197,7 +31258,7 @@ function toolAnnotations(opts = {}) {
31197
31258
  }
31198
31259
 
31199
31260
  // src/version.ts
31200
- var VERSION = "0.3.0";
31261
+ var VERSION = "0.4.0";
31201
31262
 
31202
31263
  // src/client.ts
31203
31264
  import { dirname, join } from "path";
@@ -31206,6 +31267,7 @@ var __dirname = dirname(fileURLToPath(import.meta.url));
31206
31267
  await loadDotenvSafely({ path: join(__dirname, "..", ".env"), override: false });
31207
31268
  var BASE_URL = "https://api.setlist.fm/rest";
31208
31269
  var SERVICE_NAME = "setlist.fm";
31270
+ var REQUEST_TIMEOUT_MS = 15e3;
31209
31271
  var SetlistClient = class {
31210
31272
  apiKey;
31211
31273
  configError;
@@ -31229,7 +31291,8 @@ var SetlistClient = class {
31229
31291
  baseUrl: BASE_URL,
31230
31292
  serviceName: SERVICE_NAME,
31231
31293
  retry: { count: 1, delayMs: 2e3 },
31232
- baseHeaders: lang ? { "Accept-Language": lang } : void 0
31294
+ baseHeaders: lang ? { "Accept-Language": lang } : void 0,
31295
+ timeout: REQUEST_TIMEOUT_MS
31233
31296
  });
31234
31297
  }
31235
31298
  requireKey() {
@@ -31243,11 +31306,12 @@ var SetlistClient = class {
31243
31306
  */
31244
31307
  async request(method, path, opts = {}) {
31245
31308
  const apiKey = this.requireKey();
31246
- return this.api.fetchJson(method, path, {
31309
+ const data = await this.api.fetchJson(method, path, {
31247
31310
  headers: { "x-api-key": apiKey },
31248
31311
  ...opts.query !== void 0 ? { query: opts.query } : {},
31249
31312
  ...opts.body !== void 0 ? { body: opts.body } : {}
31250
31313
  });
31314
+ return deepMapStringField(data, "eventDate", dmyToIso);
31251
31315
  }
31252
31316
  };
31253
31317
  var client = new SetlistClient();
@@ -31332,14 +31396,17 @@ function registerSetlistTools(server) {
31332
31396
  stateCode: external_exports.string().optional().describe("State code"),
31333
31397
  countryCode: external_exports.string().optional().describe("Country code (ISO 3166-1 alpha-2)"),
31334
31398
  tourName: external_exports.string().optional().describe("Tour name"),
31335
- date: external_exports.string().optional().describe("Event date in dd-MM-yyyy format (e.g. 07-08-2023)"),
31399
+ date: external_exports.string().optional().describe("Event date, ISO yyyy-MM-dd (e.g. 2025-08-28)"),
31336
31400
  year: external_exports.number().int().optional().describe("Event year"),
31337
- lastUpdated: external_exports.string().optional().describe("Only setlists updated after this UTC time (format yyyyMMddHHmmss)"),
31401
+ lastUpdated: external_exports.string().optional().describe("Only setlists updated on/after this UTC time, ISO yyyy-MM-dd or yyyy-MM-ddTHH:mm:ss"),
31338
31402
  p: page2
31339
31403
  }
31340
31404
  },
31341
31405
  async (args) => {
31342
- const data = await client.request("GET", "/1.0/search/setlists", { query: args });
31406
+ const query = { ...args };
31407
+ if (args.date) query.date = isoToDmy(args.date);
31408
+ if (args.lastUpdated) query.lastUpdated = isoToCompactTimestamp(args.lastUpdated);
31409
+ const data = await client.request("GET", "/1.0/search/setlists", { query });
31343
31410
  return textResult(data);
31344
31411
  }
31345
31412
  );
package/dist/client.js CHANGED
@@ -1,6 +1,6 @@
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, deepMapStringField, dmyToIso, } from '@chrischall/mcp-utils';
4
4
  // Load .env for local dev; silently skip if dotenv is unavailable (e.g. the
5
5
  // mcpb bundle). `loadDotenvSafely` swallows a missing dotenv module and never
6
6
  // lets .env override a host-provided value.
@@ -8,6 +8,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
8
8
  await loadDotenvSafely({ path: join(__dirname, '..', '.env'), override: false });
9
9
  const BASE_URL = 'https://api.setlist.fm/rest';
10
10
  const SERVICE_NAME = 'setlist.fm';
11
+ // Bound every request so a slow/hung upstream fails fast (createApiClient throws
12
+ // RequestTimeoutError) instead of hanging the tool call. setlist.fm normally
13
+ // answers in well under a second.
14
+ const REQUEST_TIMEOUT_MS = 15_000;
11
15
  export class SetlistClient {
12
16
  apiKey;
13
17
  configError;
@@ -37,6 +41,7 @@ export class SetlistClient {
37
41
  serviceName: SERVICE_NAME,
38
42
  retry: { count: 1, delayMs: 2000 },
39
43
  baseHeaders: lang ? { 'Accept-Language': lang } : undefined,
44
+ timeout: REQUEST_TIMEOUT_MS,
40
45
  });
41
46
  }
42
47
  requireKey() {
@@ -51,11 +56,13 @@ export class SetlistClient {
51
56
  */
52
57
  async request(method, path, opts = {}) {
53
58
  const apiKey = this.requireKey();
54
- return this.api.fetchJson(method, path, {
59
+ const data = await this.api.fetchJson(method, path, {
55
60
  headers: { 'x-api-key': apiKey },
56
61
  ...(opts.query !== undefined ? { query: opts.query } : {}),
57
62
  ...(opts.body !== undefined ? { body: opts.body } : {}),
58
63
  });
64
+ // Surface every date as ISO yyyy-MM-dd (the API returns eventDate as dd-MM-yyyy).
65
+ return deepMapStringField(data, 'eventDate', dmyToIso);
59
66
  }
60
67
  }
61
68
  /**
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { textResult } from '@chrischall/mcp-utils';
2
+ import { textResult, isoToDmy, isoToCompactTimestamp } from '@chrischall/mcp-utils';
3
3
  import { client } from '../client.js';
4
4
  import { ATTRIBUTION_NOTE } from '../attribution.js';
5
5
  // How to read a setlist's song data (per setlist.fm's guidelines), so the model
@@ -27,16 +27,24 @@ export function registerSetlistTools(server) {
27
27
  stateCode: z.string().optional().describe('State code'),
28
28
  countryCode: z.string().optional().describe('Country code (ISO 3166-1 alpha-2)'),
29
29
  tourName: z.string().optional().describe('Tour name'),
30
- date: z.string().optional().describe('Event date in dd-MM-yyyy format (e.g. 07-08-2023)'),
30
+ date: z
31
+ .string()
32
+ .optional()
33
+ .describe('Event date, ISO yyyy-MM-dd (e.g. 2025-08-28)'),
31
34
  year: z.number().int().optional().describe('Event year'),
32
35
  lastUpdated: z
33
36
  .string()
34
37
  .optional()
35
- .describe('Only setlists updated after this UTC time (format yyyyMMddHHmmss)'),
38
+ .describe('Only setlists updated on/after this UTC time, ISO yyyy-MM-dd or yyyy-MM-ddTHH:mm:ss'),
36
39
  p: page,
37
40
  },
38
41
  }, async (args) => {
39
- const data = await client.request('GET', '/1.0/search/setlists', { query: args });
42
+ const query = { ...args };
43
+ if (args.date)
44
+ query.date = isoToDmy(args.date);
45
+ if (args.lastUpdated)
46
+ query.lastUpdated = isoToCompactTimestamp(args.lastUpdated);
47
+ const data = await client.request('GET', '/1.0/search/setlists', { query });
40
48
  return textResult(data);
41
49
  });
42
50
  server.registerTool('setlist_get_setlist', {
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.3.0'; // x-release-please-version
6
+ export const VERSION = '0.4.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "setlist-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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>",
@@ -42,7 +42,7 @@
42
42
  "test:coverage": "vitest run --coverage"
43
43
  },
44
44
  "dependencies": {
45
- "@chrischall/mcp-utils": "^0.5.2",
45
+ "@chrischall/mcp-utils": "^0.6.0",
46
46
  "@modelcontextprotocol/sdk": "^1.29.0",
47
47
  "dotenv": "^17.4.0",
48
48
  "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.3.0",
9
+ "version": "0.4.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "setlist-mcp",
14
- "version": "0.3.0",
14
+ "version": "0.4.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },