setlist-mcp 0.1.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.
package/dist/client.js ADDED
@@ -0,0 +1,67 @@
1
+ import { dirname, join } from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import { loadDotenvSafely, readEnvVar, createApiClient, } from '@chrischall/mcp-utils';
4
+ // Load .env for local dev; silently skip if dotenv is unavailable (e.g. the
5
+ // mcpb bundle). `loadDotenvSafely` swallows a missing dotenv module and never
6
+ // lets .env override a host-provided value.
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ await loadDotenvSafely({ path: join(__dirname, '..', '.env'), override: false });
9
+ const BASE_URL = 'https://api.setlist.fm/rest';
10
+ const SERVICE_NAME = 'setlist.fm';
11
+ export class SetlistClient {
12
+ apiKey;
13
+ configError;
14
+ api;
15
+ /**
16
+ * Defer the config error so the server can still start (and answer the host's
17
+ * install-time tools/list smoke test) when SETLIST_API_KEY isn't set yet.
18
+ * Tool calls re-raise the error at request time via {@link requireKey}.
19
+ */
20
+ constructor() {
21
+ const key = readEnvVar('SETLIST_API_KEY');
22
+ if (!key) {
23
+ this.apiKey = null;
24
+ this.configError = new Error('SETLIST_API_KEY environment variable is required');
25
+ }
26
+ else {
27
+ this.apiKey = key;
28
+ this.configError = null;
29
+ }
30
+ // setlist.fm authenticates with an `x-api-key` header (not a Bearer token),
31
+ // attached per-request in `request()`. We localize city/country names via an
32
+ // optional `Accept-Language`. `Accept: application/json` is already the
33
+ // fetchJson default (the API serves XML otherwise).
34
+ const lang = readEnvVar('SETLIST_ACCEPT_LANGUAGE');
35
+ this.api = createApiClient({
36
+ baseUrl: BASE_URL,
37
+ serviceName: SERVICE_NAME,
38
+ retry: { count: 1, delayMs: 2000 },
39
+ baseHeaders: lang ? { 'Accept-Language': lang } : undefined,
40
+ });
41
+ }
42
+ requireKey() {
43
+ if (this.configError)
44
+ throw this.configError;
45
+ return this.apiKey;
46
+ }
47
+ /**
48
+ * Issue a request with the `x-api-key` header attached. `requireKey()` runs
49
+ * here (not in the constructor) so a missing key surfaces at request time,
50
+ * keeping the deferred-config-error pattern intact.
51
+ */
52
+ async request(method, path, opts = {}) {
53
+ const apiKey = this.requireKey();
54
+ return this.api.fetchJson(method, path, {
55
+ headers: { 'x-api-key': apiKey },
56
+ ...(opts.query !== undefined ? { query: opts.query } : {}),
57
+ ...(opts.body !== undefined ? { body: opts.body } : {}),
58
+ });
59
+ }
60
+ }
61
+ /**
62
+ * Module-level singleton shared by every tool module. Constructing it here (not
63
+ * in `index.ts`) keeps the deferred-config-error pattern: the server boots and
64
+ * answers the host's install-time tools/list smoke test even when the API key
65
+ * is absent — the error only surfaces on the first request.
66
+ */
67
+ export const client = new SetlistClient();
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import { runMcp } from '@chrischall/mcp-utils';
3
+ import { VERSION } from './version.js';
4
+ import { registerArtistTools } from './tools/artists.js';
5
+ import { registerSetlistTools } from './tools/setlists.js';
6
+ import { registerVenueTools } from './tools/venues.js';
7
+ import { registerGeoTools } from './tools/geo.js';
8
+ import { registerUserTools } from './tools/users.js';
9
+ import { registerUtilityTools } from './tools/utilities.js';
10
+ // The setlist.fm client is a module-level singleton (imported by each tool
11
+ // module) that defers its config error to the first request. That preserves the
12
+ // deferred-config-error pattern: the server boots and answers the host's
13
+ // install-time tools/list smoke test even when SETLIST_API_KEY is absent — the
14
+ // configuration error only surfaces on the first tool call.
15
+ await runMcp({
16
+ name: 'setlist-mcp',
17
+ version: VERSION,
18
+ banner: '[setlist-mcp] This project was developed and is maintained by AI (Claude Opus 4.8). Use at your own discretion.',
19
+ tools: [
20
+ registerArtistTools,
21
+ registerSetlistTools,
22
+ registerVenueTools,
23
+ registerGeoTools,
24
+ registerUserTools,
25
+ registerUtilityTools,
26
+ ],
27
+ });
@@ -0,0 +1,50 @@
1
+ import { z } from 'zod';
2
+ import { textResult } from '@chrischall/mcp-utils';
3
+ import { client } from '../client.js';
4
+ const page = z
5
+ .number()
6
+ .int()
7
+ .positive()
8
+ .optional()
9
+ .describe('Result page number (defaults to 1)');
10
+ export function registerArtistTools(server) {
11
+ server.registerTool('setlist_search_artists', {
12
+ description: "Search setlist.fm for artists by name or MusicBrainz ID. Returns matching artists with their MusicBrainz ID (mbid) — use that mbid with setlist_get_artist or setlist_get_artist_setlists.",
13
+ annotations: { readOnlyHint: true },
14
+ inputSchema: {
15
+ artistName: z.string().optional().describe('Artist name to search for'),
16
+ artistMbid: z.string().optional().describe("Artist's MusicBrainz ID (mbid)"),
17
+ sort: z
18
+ .enum(['sortName', 'relevance'])
19
+ .optional()
20
+ .describe('Sort order (sortName = default, or relevance)'),
21
+ p: page,
22
+ },
23
+ }, async ({ artistName, artistMbid, sort, p }) => {
24
+ const data = await client.request('GET', '/1.0/search/artists', {
25
+ query: { artistName, artistMbid, sort, p },
26
+ });
27
+ return textResult(data);
28
+ });
29
+ server.registerTool('setlist_get_artist', {
30
+ description: "Get a setlist.fm artist by their MusicBrainz ID (mbid).",
31
+ annotations: { readOnlyHint: true },
32
+ inputSchema: {
33
+ mbid: z.string().describe("Artist's MusicBrainz ID (mbid)"),
34
+ },
35
+ }, async ({ mbid }) => {
36
+ const data = await client.request('GET', `/1.0/artist/${encodeURIComponent(mbid)}`);
37
+ return textResult(data);
38
+ });
39
+ server.registerTool('setlist_get_artist_setlists', {
40
+ description: "Get an artist's setlists (most recent first) by their MusicBrainz ID (mbid). Paginated via `p`.",
41
+ annotations: { readOnlyHint: true },
42
+ inputSchema: {
43
+ mbid: z.string().describe("Artist's MusicBrainz ID (mbid)"),
44
+ p: page,
45
+ },
46
+ }, async ({ mbid, p }) => {
47
+ const data = await client.request('GET', `/1.0/artist/${encodeURIComponent(mbid)}/setlists`, { query: { p } });
48
+ return textResult(data);
49
+ });
50
+ }
@@ -0,0 +1,42 @@
1
+ import { z } from 'zod';
2
+ import { textResult } from '@chrischall/mcp-utils';
3
+ import { client } from '../client.js';
4
+ const page = z
5
+ .number()
6
+ .int()
7
+ .positive()
8
+ .optional()
9
+ .describe('Result page number (defaults to 1)');
10
+ export function registerGeoTools(server) {
11
+ server.registerTool('setlist_search_cities', {
12
+ description: "Search setlist.fm for cities by name and/or location. Returns cities with their geoId — use it as cityId in setlist_search_setlists / setlist_search_venues, or with setlist_get_city.",
13
+ annotations: { readOnlyHint: true },
14
+ inputSchema: {
15
+ name: z.string().optional().describe('City name'),
16
+ country: z.string().optional().describe("City's country"),
17
+ state: z.string().optional().describe('State the city lies in'),
18
+ stateCode: z.string().optional().describe('State code the city lies in'),
19
+ p: page,
20
+ },
21
+ }, async (args) => {
22
+ const data = await client.request('GET', '/1.0/search/cities', { query: args });
23
+ return textResult(data);
24
+ });
25
+ server.registerTool('setlist_get_city', {
26
+ description: "Get a city by its geoId.",
27
+ annotations: { readOnlyHint: true },
28
+ inputSchema: {
29
+ geoId: z.string().describe("City's geoId"),
30
+ },
31
+ }, async ({ geoId }) => {
32
+ const data = await client.request('GET', `/1.0/city/${encodeURIComponent(geoId)}`);
33
+ return textResult(data);
34
+ });
35
+ server.registerTool('setlist_search_countries', {
36
+ description: "List all countries supported by setlist.fm, with their ISO country codes. Use a code as countryCode in setlist_search_setlists.",
37
+ annotations: { readOnlyHint: true },
38
+ }, async () => {
39
+ const data = await client.request('GET', '/1.0/search/countries');
40
+ return textResult(data);
41
+ });
42
+ }
@@ -0,0 +1,57 @@
1
+ import { z } from 'zod';
2
+ import { textResult } from '@chrischall/mcp-utils';
3
+ import { client } from '../client.js';
4
+ const page = z
5
+ .number()
6
+ .int()
7
+ .positive()
8
+ .optional()
9
+ .describe('Result page number (defaults to 1)');
10
+ export function registerSetlistTools(server) {
11
+ server.registerTool('setlist_search_setlists', {
12
+ description: "Search setlist.fm for concert setlists. Filter by any combination of artist, venue, city, country, tour, date, or year. Returns setlists with their songs and event details. Provide at least one filter.",
13
+ annotations: { readOnlyHint: true },
14
+ inputSchema: {
15
+ artistName: z.string().optional().describe('Artist name'),
16
+ artistMbid: z.string().optional().describe("Artist's MusicBrainz ID (mbid)"),
17
+ venueName: z.string().optional().describe('Venue name'),
18
+ venueId: z.string().optional().describe('Venue ID'),
19
+ cityName: z.string().optional().describe('City name'),
20
+ cityId: z.string().optional().describe("City's geoId"),
21
+ state: z.string().optional().describe('State name'),
22
+ stateCode: z.string().optional().describe('State code'),
23
+ countryCode: z.string().optional().describe('Country code (ISO 3166-1 alpha-2)'),
24
+ tourName: z.string().optional().describe('Tour name'),
25
+ date: z.string().optional().describe('Event date in dd-MM-yyyy format (e.g. 07-08-2023)'),
26
+ year: z.number().int().optional().describe('Event year'),
27
+ lastUpdated: z
28
+ .string()
29
+ .optional()
30
+ .describe('Only setlists updated after this UTC time (format yyyyMMddHHmmss)'),
31
+ p: page,
32
+ },
33
+ }, async (args) => {
34
+ const data = await client.request('GET', '/1.0/search/setlists', { query: args });
35
+ return textResult(data);
36
+ });
37
+ server.registerTool('setlist_get_setlist', {
38
+ description: "Get a setlist.fm setlist by its ID, including the full song list and event details.",
39
+ annotations: { readOnlyHint: true },
40
+ inputSchema: {
41
+ setlistId: z.string().describe('Setlist ID (e.g. 63de4613)'),
42
+ },
43
+ }, async ({ setlistId }) => {
44
+ const data = await client.request('GET', `/1.0/setlist/${encodeURIComponent(setlistId)}`);
45
+ return textResult(data);
46
+ });
47
+ server.registerTool('setlist_get_setlist_version', {
48
+ description: "Get a specific historical version of a setlist by its version ID. Setlists are wiki-edited; each edit has a version ID returned in a setlist's `versionId` field.",
49
+ annotations: { readOnlyHint: true },
50
+ inputSchema: {
51
+ versionId: z.string().describe('Setlist version ID'),
52
+ },
53
+ }, async ({ versionId }) => {
54
+ const data = await client.request('GET', `/1.0/setlist/version/${encodeURIComponent(versionId)}`);
55
+ return textResult(data);
56
+ });
57
+ }
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ import { textResult } from '@chrischall/mcp-utils';
3
+ import { client } from '../client.js';
4
+ const page = z
5
+ .number()
6
+ .int()
7
+ .positive()
8
+ .optional()
9
+ .describe('Result page number (defaults to 1)');
10
+ export function registerUserTools(server) {
11
+ server.registerTool('setlist_get_user', {
12
+ description: "Get a setlist.fm user's public profile by their userId (their setlist.fm username).",
13
+ annotations: { readOnlyHint: true },
14
+ inputSchema: {
15
+ userId: z.string().describe('setlist.fm userId (username)'),
16
+ },
17
+ }, async ({ userId }) => {
18
+ const data = await client.request('GET', `/1.0/user/${encodeURIComponent(userId)}`);
19
+ return textResult(data);
20
+ });
21
+ server.registerTool('setlist_get_user_attended', {
22
+ description: "Get the concerts a setlist.fm user has marked as attended. Paginated via `p`.",
23
+ annotations: { readOnlyHint: true },
24
+ inputSchema: {
25
+ userId: z.string().describe('setlist.fm userId (username)'),
26
+ p: page,
27
+ },
28
+ }, async ({ userId, p }) => {
29
+ const data = await client.request('GET', `/1.0/user/${encodeURIComponent(userId)}/attended`, { query: { p } });
30
+ return textResult(data);
31
+ });
32
+ server.registerTool('setlist_get_user_edited', {
33
+ description: "Get the setlists a setlist.fm user has created or edited. Paginated via `p`.",
34
+ annotations: { readOnlyHint: true },
35
+ inputSchema: {
36
+ userId: z.string().describe('setlist.fm userId (username)'),
37
+ p: page,
38
+ },
39
+ }, async ({ userId, p }) => {
40
+ const data = await client.request('GET', `/1.0/user/${encodeURIComponent(userId)}/edited`, { query: { p } });
41
+ return textResult(data);
42
+ });
43
+ }
@@ -0,0 +1,41 @@
1
+ import { textResult, toolAnnotations, messageOf } from '@chrischall/mcp-utils';
2
+ import { client } from '../client.js';
3
+ export function registerUtilityTools(server) {
4
+ server.registerTool('setlist_healthcheck', {
5
+ title: 'Verify setlist.fm API key + connectivity',
6
+ description: 'Confirm the API key is configured and works by calling the setlist.fm countries endpoint. Reports {ok, authenticated, country_count} with a plain-English hint distinguishing "no key" vs "bad key" vs "API error". Read-only.',
7
+ annotations: toolAnnotations({
8
+ title: 'Verify setlist.fm API key + connectivity',
9
+ readOnly: true,
10
+ idempotent: true,
11
+ openWorld: true,
12
+ }),
13
+ inputSchema: {},
14
+ }, async () => {
15
+ try {
16
+ const data = await client.request('GET', '/1.0/search/countries');
17
+ const count = data.total ?? data.country?.length ?? 0;
18
+ return textResult({
19
+ ok: true,
20
+ authenticated: true,
21
+ country_count: count,
22
+ hint: 'API key is valid and the setlist.fm API is reachable.',
23
+ });
24
+ }
25
+ catch (e) {
26
+ const msg = messageOf(e);
27
+ const noKey = /environment variable is required/.test(msg);
28
+ const badKey = /\b(401|403)\b/.test(msg);
29
+ return textResult({
30
+ ok: false,
31
+ authenticated: false,
32
+ error: msg,
33
+ hint: noKey
34
+ ? 'Set SETLIST_API_KEY (in .env or the MCP host env), then retry. Apply for a key at https://www.setlist.fm/settings/api.'
35
+ : badKey
36
+ ? 'The API key was rejected (401/403). Verify SETLIST_API_KEY is correct and active.'
37
+ : 'The setlist.fm API call failed — it may be rate-limited or temporarily unavailable. Retry shortly.',
38
+ });
39
+ }
40
+ });
41
+ }
@@ -0,0 +1,48 @@
1
+ import { z } from 'zod';
2
+ import { textResult } from '@chrischall/mcp-utils';
3
+ import { client } from '../client.js';
4
+ const page = z
5
+ .number()
6
+ .int()
7
+ .positive()
8
+ .optional()
9
+ .describe('Result page number (defaults to 1)');
10
+ export function registerVenueTools(server) {
11
+ server.registerTool('setlist_search_venues', {
12
+ description: "Search setlist.fm for venues by name and/or location. Returns matching venues with their venue ID — use it with setlist_get_venue or setlist_get_venue_setlists.",
13
+ annotations: { readOnlyHint: true },
14
+ inputSchema: {
15
+ name: z.string().optional().describe('Venue name'),
16
+ cityName: z.string().optional().describe('City the venue is in'),
17
+ cityId: z.string().optional().describe("City's geoId"),
18
+ state: z.string().optional().describe('State name'),
19
+ stateCode: z.string().optional().describe('State code'),
20
+ country: z.string().optional().describe("Venue's country"),
21
+ p: page,
22
+ },
23
+ }, async (args) => {
24
+ const data = await client.request('GET', '/1.0/search/venues', { query: args });
25
+ return textResult(data);
26
+ });
27
+ server.registerTool('setlist_get_venue', {
28
+ description: "Get a setlist.fm venue by its ID.",
29
+ annotations: { readOnlyHint: true },
30
+ inputSchema: {
31
+ venueId: z.string().describe('Venue ID'),
32
+ },
33
+ }, async ({ venueId }) => {
34
+ const data = await client.request('GET', `/1.0/venue/${encodeURIComponent(venueId)}`);
35
+ return textResult(data);
36
+ });
37
+ server.registerTool('setlist_get_venue_setlists', {
38
+ description: "Get setlists performed at a venue, by venue ID (most recent first). Paginated via `p`.",
39
+ annotations: { readOnlyHint: true },
40
+ inputSchema: {
41
+ venueId: z.string().describe('Venue ID'),
42
+ p: page,
43
+ },
44
+ }, async ({ venueId, p }) => {
45
+ const data = await client.request('GET', `/1.0/venue/${encodeURIComponent(venueId)}/setlists`, { query: { p } });
46
+ return textResult(data);
47
+ });
48
+ }
@@ -0,0 +1,6 @@
1
+ // Single source of truth for the server version. release-please bumps the
2
+ // string below via the `x-release-please-version` marker (registered in
3
+ // release-please-config.json's `extra-files`), and `versionSyncTest` guards
4
+ // that it stays equal to package.json. Import VERSION wherever the version is
5
+ // needed rather than re-declaring it.
6
+ export const VERSION = '0.1.0'; // x-release-please-version
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "setlist-mcp",
3
+ "version": "0.1.0",
4
+ "mcpName": "io.github.chrischall/setlist-mcp",
5
+ "description": "setlist.fm MCP server for Claude — developed and maintained by AI (Claude Code)",
6
+ "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/chrischall/setlist-mcp.git"
10
+ },
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "mcp",
14
+ "model-context-protocol",
15
+ "claude",
16
+ "ai",
17
+ "setlist.fm",
18
+ "setlists",
19
+ "concerts",
20
+ "live-music",
21
+ "gigs",
22
+ "tours",
23
+ "musicbrainz"
24
+ ],
25
+ "type": "module",
26
+ "bin": {
27
+ "setlist-mcp": "dist/index.js"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ ".claude-plugin",
32
+ "SKILL.md",
33
+ ".mcp.json",
34
+ "server.json"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc && npm run bundle",
38
+ "bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --outfile=dist/bundle.js",
39
+ "dev": "node dist/index.js",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest",
42
+ "test:coverage": "vitest run --coverage"
43
+ },
44
+ "dependencies": {
45
+ "@chrischall/mcp-utils": "^0.5.2",
46
+ "@modelcontextprotocol/sdk": "^1.29.0",
47
+ "dotenv": "^17.4.0",
48
+ "zod": "^4.4.2"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^25.5.2",
52
+ "@vitest/coverage-v8": "^4.1.2",
53
+ "esbuild": "^0.28.0",
54
+ "typescript": "^6.0.2",
55
+ "vitest": "^4.1.2"
56
+ }
57
+ }
package/server.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.chrischall/setlist-mcp",
4
+ "description": "setlist.fm concert data for Claude — setlists, artists, venues, tours, and cities",
5
+ "repository": {
6
+ "url": "https://github.com/chrischall/setlist-mcp",
7
+ "source": "github"
8
+ },
9
+ "version": "0.1.0",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "setlist-mcp",
14
+ "version": "0.1.0",
15
+ "transport": {
16
+ "type": "stdio"
17
+ },
18
+ "environmentVariables": [
19
+ {
20
+ "name": "SETLIST_API_KEY",
21
+ "description": "Your setlist.fm API key (apply at setlist.fm/settings/api)",
22
+ "isRequired": true,
23
+ "format": "string",
24
+ "isSecret": true
25
+ },
26
+ {
27
+ "name": "SETLIST_ACCEPT_LANGUAGE",
28
+ "description": "Optional language for city/country names (en, es, fr, de, pt, tr, it, pl)",
29
+ "isRequired": false,
30
+ "format": "string",
31
+ "isSecret": false
32
+ }
33
+ ]
34
+ }
35
+ ]
36
+ }