musicbrainz-mcp-server
by cyanheads
README.md
<div align="center">
<h1>@cyanheads/musicbrainz-mcp-server</h1>
<p><b>Search artists, releases, recordings, works, and labels; traverse relationships; resolve ISRC/ISWC/barcode; fetch cover art via MCP. STDIO or Streamable HTTP.</b>
<div>10 Tools • 1 Entity Resource • 1 Skill</div>
</p>
</div>
<div align="center">
[](./CHANGELOG.md) [](./LICENSE) [](https://modelcontextprotocol.io/) [](https://www.typescriptlang.org/) [](https://bun.sh/)
</div>
<div align="center">
[](https://github.com/cyanheads/musicbrainz-mcp-server/releases/latest/download/musicbrainz-mcp-server.mcpb) [](https://cursor.com/en/install-mcp?name=musicbrainz-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvbXVzaWNicmFpbnotbWNwLXNlcnZlciJdfQ==) [](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22musicbrainz-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads%2Fmusicbrainz-mcp-server%22%5D%7D)
[](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
</div>
<div align="center">
**Public Hosted Server:** [https://musicbrainz.caseyjhand.com/mcp](https://musicbrainz.caseyjhand.com/mcp)
</div>
---
## Overview
Open music metadata from MusicBrainz and the Cover Art Archive: artists, releases, recordings, works, and labels. Search by name, look up by MBID, ISRC, ISWC, or barcode, page through complete discographies, and fetch cover art.
### Tools
| Tool | Description |
|:---|:---|
| `musicbrainz_search_entities` | Lucene search over one entity type, returning ranked MBIDs with relevance scores |
| `musicbrainz_get_artist` | Artist profile: areas, IPI/ISNI, life span, discography, relationships, external links |
| `musicbrainz_get_release_group` | Release-group (the album across its editions): types, first-release date, editions, cover-art availability |
| `musicbrainz_get_release` | One edition: tracklist with artist credits, label and catalog number, barcode, packaging, external links |
| `musicbrainz_get_recording` | Recording (one performance): length, ISRCs, releases it appears on, performer and production credits |
| `musicbrainz_get_work` | Work (a composition): type, ISWCs, writers, movements, arrangements, and recordings |
| `musicbrainz_get_label` | Label: area, IPI/ISNI, life span, label code, label relationships, external links |
| `musicbrainz_lookup_identifier` | Resolve an ISRC, ISWC, or barcode to recordings, works, or releases |
| `musicbrainz_browse_entities` | Page through every entity linked to a parent MBID |
| `musicbrainz_get_cover_art` | Cover Art Archive images for a release or release-group |
### Resources
| Resource | Description |
|:---|:---|
| `musicbrainz://{entity_type}/{mbid}` | One entity as raw MusicBrainz JSON, with the matching `get_*` tool's linked data folded in |
The `get_*` tools provide normalized lookup views for tool-only clients. The entity resource has no list; find MBIDs with `musicbrainz_search_entities`.
### Agent skill
[`musicbrainz-guide`](skills/musicbrainz-guide/SKILL.md) covers identity checks, edition selection, discographies, credits, artwork, and Lucene queries. It ships with npm, Docker, MCPB, and plugin installs, and is available through the MCP Skills extension and `skill://` resources.
## Capability reference
### `musicbrainz_search_entities` <sub>tool</sub>
- One `entityType` per call (`artist`, `release-group`, `release`, `recording`, `work`, `label`) and a Lucene `query` with field scoping (`artist:radiohead AND country:GB`); positive integer `limit` (default 25, values above 100 clamp to 100 with a notice), `offset` for paging
- Returns the effective `offset` and upstream `totalCount`. Continuation uses `offset + rows returned`, including short pages. An empty page at or beyond a positive total reports exhaustion; an unexpectedly empty page before the total reports the discrepancy without suggesting a nonadvancing offset
- Search exposes only the first 500 matches. A page crossing that boundary is shortened with a notice; offsets of 500 or more fail as `search_window_exceeded`. Narrow the query or browse a linked set to reach further matches; `totalCount` remains the full upstream count
- Each hit carries `mbid`, `name`, and the raw 0–100 `score` in MusicBrainz order, plus type-specific fields such as `artistCredit`, `isrcs`, `iswcs`, and `length`. Release rows use `status` (Official, Bootleg, …); artist, release-group, work, and label subtypes use `type`. A whitespace-only query fails as `blank_query`
- Release-group hits also carry available `secondaryTypes`; recording hits carry available `firstReleaseDate`, including partial dates
---
### `musicbrainz_get_artist` <sub>tool</sub>
- `mbid`, plus `inc_release_groups` and `inc_relationships` (both default `true`) to drop the heavier sub-resources
- Type, gender, country, area, `lifeSpan`, aliases, and tags, plus `releaseGroups` (one page), `relationships` (band membership, collaborations), and `externalLinks` (Wikidata, Discogs, official site)
- `areaId` can be passed to browse `link.area`; `beginArea` and `endArea` retain available MBID/name members. `ipis` and `isnis` preserve the catalog's identifier strings, including leading zeros
---
### `musicbrainz_get_release_group` <sub>tool</sub>
- `mbid` only; returns `primaryType`, `secondaryTypes`, `firstReleaseDate`, `artistCredit` with `artistCreditString`, tags, and one page of `releases` (editions)
- `externalLinks` carries album-level identities such as Wikidata and Discogs masters
- `coverArt` (`exists`, `count`, `front`, `back`) comes from a live Cover Art Archive check; when that check fails the field is omitted with a notice, never reported as "no art"
---
### `musicbrainz_get_release` <sub>tool</sub>
- `mbid`, `media_offset` (default 0), and `media_limit` (default 20, no maximum); returns ordered `media` with complete tracks per medium (`position`, `title`, `length` as m:ss, `recordingId`), `labelInfo` (label and `catalogNumber`), `barcode`, `status`, `date`, `country`, `packaging`, `language`/`script`, and `releaseGroupId`
- `mediaTotal` and `trackTotal` count all normalized media and track occurrences; upstream per-medium `trackCount` is preserved. `mediaOffset` identifies the window. When more media remain, `truncated`, `shown`, `cap`, and a notice give the next `media_offset`; empty end pages report exhaustion. The release resource retains the complete raw tracklist
- `coverArt` is the availability stub from the release record; call `musicbrainz_get_cover_art` for image URLs
- Tracks retain their own available `artistCredit` and `artistCreditString`, independently of release or recording credits. `externalLinks` belongs to the selected edition
---
### `musicbrainz_get_recording` <sub>tool</sub>
- `mbid`, plus `inc_relationships` (default `true`) for performer, producer, and engineer credits, work links, recording-to-recording relationships such as samples and remixes, and external links
- Returns `length`, `isrcs`, `artistCredit`, `firstReleaseDate`, one upstream page of `releases` (up to 25), `relationships`, and `externalLinks`
- `releaseAppearanceSummary` reports `shown`, `cap: 25`, and `completeness`: a short present page is `complete`, a full or absent page without an exact count is `unknown`, and an exact upstream count exceeding the returned page proves `truncated`. `totalCount` is omitted when unknown; no count request is added. Enumerate appearances with `musicbrainz_browse_entities` (`target_type=release`, `link.recording=<mbid>`, `offset=0`), because embedded and browse order may differ
---
### `musicbrainz_get_work` <sub>tool</sub>
- `mbid`, plus `inc_relationships` (default `true`) for writer, composer, and lyricist credits, movements and arrangements, the recordings that perform it, and external links
- Returns `type`, `languages`, `iswcs`, aliases, and tags. Only the first 25 recording relationships are kept (other relationships and links are all kept); `totalCount` gives the full count, and `truncated`, `shown`, and `cap` appear when some were cut
- `attributes` preserves ordered type/value rows, including musical key, catalogue metadata and society work IDs. Repeated types and exact string values are retained, even with relationships disabled
- To list every recording of a work, browse `recording` by `work`, then call `musicbrainz_get_recording` for relationship attributes and dates
- Follow a `relationships` entry with `targetType: "work"` using its `targetId` in another `musicbrainz_get_work` call. `direction` distinguishes parent/child links; optional `orderingKey` preserves the upstream sequence key (including zero), and `targetDisambiguation` distinguishes arrangements. Relationships retain upstream array order, which need not match movement order
---
### `musicbrainz_get_label` <sub>tool</sub>
- `mbid` only; returns type, country, area and `areaId`, `ipis`, `isnis`, `lifeSpan`, `labelCode` (the LC number without its prefix), aliases, tags, label `relationships`, and `externalLinks`
- Releases are not embedded; list them with `musicbrainz_browse_entities` (`target_type=release`, `link.label`)
---
### `musicbrainz_lookup_identifier` <sub>tool</sub>
- `id_type` `isrc` → recordings, `iswc` → works, `barcode` → releases; a barcode is UPC/EAN digits and returns up to 25 ranked matches, each with a `score`
- ISRC accepts compact `GBAYE9701274` or printed `GB-AYE-97-01274`, ignoring case and surrounding whitespace. ISWC accepts `T-010.140.236-1` or `T0101402361`, with optional group punctuation, ignoring whitespace and case. Results echo the canonical compact ISRC or `T-DDD.DDD.DDD-C` ISWC; malformed codes fail before a request
- `result.kind` (`recordings` | `works` | `releases`) names the arm that came back; failures are `invalid_identifier` or `identifier_not_found`
---
### `musicbrainz_browse_entities` <sub>tool</sub>
- `target_type` plus exactly one parent MBID in `link`; `releaseGroup`/`release_group` alias `release-group`. Positive integer `limit` defaults to 25; values above 100 clamp to 100 with a notice. Set `MUSICBRAINZ_CLAMP_OVERSIZED_LIMITS=off` to enforce maximum 100 for both search and browse
- Reports upstream `totalCount`; while more remain, `truncated` is set and a nonempty page gives the next `offset` using actual rows returned. Empty pages distinguish zero linked entities, exhaustion, and an unexpected empty page before the total. Clamp and paging guidance share one notice. Release rows use `status`; other entities use `type`
- A missing, extra, or malformed link fails as `invalid_link`, an unknown parent as `entity_not_found`. For ordered tracks of an edition use `musicbrainz_get_release`
- Rows retain available release-group `secondaryTypes` and recording `firstReleaseDate`. `Album` alone does not distinguish studio albums from live albums or compilations
- Optional `type` and `status` filters accept pipe-separated values with case and surrounding token whitespace normalized, echoed as `type`/`status`. Primary types union; primary and secondary categories intersect. Release-group-by-artist requires every supplied secondary type. Empty optional filters are ignored; unsupported target/parent combinations and invalid tokens fail as `invalid_filter` before fetching
- `type` is supported for release-groups by artist, and releases by artist, label, recording, or `track_artist`. `status` is supported for those release parents plus release-group. Area browse supports neither filter. Preserve applied filters when paging
- `link.track_artist` finds releases crediting an artist on tracks but not in the release credit. Obtain area MBIDs from `get_artist` or `get_label` `areaId`; those tools do not resolve arbitrary area MBIDs
| Target | Accepted parent keys |
|:---|:---|
| `artist` | `area`, `recording`, `release`, `release-group`, `work` |
| `label` | `area`, `release` |
| `release-group` | `artist`, `release` |
| `release` | `area`, `artist`, `label`, `recording`, `release-group`, `track_artist` |
| `recording` | `artist`, `release`, `work` |
| `work` | `artist` |
---
### `musicbrainz_get_cover_art` <sub>tool</sub>
- `mbid` and `entity_type`: `release` (default) or `release-group`, which resolves to a `representativeRelease`; `limit` defaults to 25 and clamps above 100, and `offset` defaults to 0. These windows apply after the archive index is cached and are independent of the search/browse clamp setting
- `images` retain archive order with `front`/`back`, `types`, `imageUrl`, and 250/500/1200px thumbnails. `totalCount` is exact, `offset` identifies the window, and `hasArt` reflects the full archive even on an empty end page. A single notice combines any clamp, continuation, exhaustion, or no-art guidance; `truncated`, `shown`, and `cap` appear when more images remain
- An unknown MBID fails as `entity_not_found`; if MusicBrainz can't confirm the MBID exists, the empty set comes back with a notice saying so
---
### `musicbrainz://{entity_type}/{mbid}` <sub>resource</sub>
- `entity_type` is `artist`, `release-group`, `release`, `recording`, `work`, or `label`; returns the raw MusicBrainz record as `application/json`
- Fetches the same default linked data as the matching `get_*` tool; failures are `invalid_mbid` and `entity_not_found`
- Work JSON includes raw work-to-work relationships and keeps the first 25 recording relationships; `recordingRelationshipSummary` gives the exact recording count and whether any were cut
- Recording JSON carries the same `releaseAppearanceSummary` as the recording tool without changing cached raw data. Release JSON retains every medium and track; the tool's media window does not apply to resources
## Features
Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): stdio and Streamable HTTP transports, pluggable auth (`none` / `jwt` / `oauth`), swappable storage (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`), structured logging with optional OpenTelemetry tracing.
MusicBrainz-specific:
- Keyless client for the MusicBrainz Web Service v2 and the Cover Art Archive. Every request carries the descriptive `User-Agent` MusicBrainz requires, `musicbrainz-mcp-server/<version> ( <contact> )`, with `MUSICBRAINZ_CONTACT` as the contact
- Entities are addressed by MBID (a hyphenated UUID); uppercase and surrounding whitespace are normalized. Malformed or all-zero IDs fail as `invalid_mbid` (`invalid_link` for browse); a well-formed unknown ID fails as `entity_not_found`
- MusicBrainz calls are held to ~1 request/second across all clients of an instance, so long browse runs take a while
- Lookups fold discography, relationships, tracklists, and external links into one request; responses are cached (24 h by default) and transient upstream errors are retried with backoff
Agent-friendly output:
- Artist release-groups, release-group editions, and recording release appearances embed one page (25); browse enumerates the full set. Recording appearances disclose completeness without a count request. Work recording relationships are capped at 25 with the exact count reported; labels embed no releases
- Search echoes the effective query; search and browse report the exact upstream total. Missing or invalid totals fail as malformed upstream responses instead of inventing a count
- Discriminated outputs: `musicbrainz_lookup_identifier` returns a `kind`-tagged union, so callers branch on data, not string parsing
- Raw upstream relevance `score`, not a derived confidence; missing upstream fields stay absent rather than invented
## Getting started
Connect to the hosted instance:
```sh
claude mcp add --transport http musicbrainz-mcp-server https://musicbrainz.caseyjhand.com/mcp
```
Or run it locally:
```sh
claude mcp add musicbrainz-mcp-server -- npx -y @cyanheads/musicbrainz-mcp-server
```
For other clients, add this to the MCP config file:
```json
{
"mcpServers": {
"musicbrainz-mcp-server": {
"command": "npx",
"args": ["-y", "@cyanheads/musicbrainz-mcp-server"]
}
}
}
```
No API key or account is required. Set `MUSICBRAINZ_CONTACT` to your own email or URL for shared or hosted deployments. For Docker or a source build, see [Running the server](#running-the-server).
## Configuration
| Variable | Description | Default |
|:---|:---|:---|
| `MUSICBRAINZ_CONTACT` | Email or URL sent in the `User-Agent`; set your own on shared or hosted instances. | repo URL |
| `MUSICBRAINZ_BASE_URL` | MusicBrainz Web Service v2 base URL (e.g. a private mirror). | `https://musicbrainz.org/ws/2` |
| `MUSICBRAINZ_RATE_LIMIT_RPS` | Client-side requests-per-second ceiling (0.1–50). | `1` |
| `MUSICBRAINZ_CACHE_TTL` | Response cache TTL in seconds; `0` disables caching. | `86400` |
| `MUSICBRAINZ_TIMEOUT_MS` | Per-request HTTP timeout, in ms. | `30000` |
| `MUSICBRAINZ_MAX_RETRIES` | Retry attempts for transient upstream failures. | `3` |
| `MUSICBRAINZ_CLAMP_OVERSIZED_LIMITS` | Clamp search/browse limits above 100 with a notice; `off` restores maximum-100 schema validation. Strict boolean (`true`/`false`, `on`/`off`, `1`/`0`, `yes`/`no`). | `true` |
| `COVER_ART_BASE_URL` | Cover Art Archive base URL. | `https://coverartarchive.org` |
| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
| `MCP_HTTP_PORT` | HTTP server port. | `3010` |
| `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth`. | `none` |
| `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.). | `info` |
| `OTEL_ENABLED` | Enable [OpenTelemetry](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry). | `false` |
See [`.env.example`](./.env.example) for the full list of optional overrides.
## Running the server
### Local development
Requires [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
```sh
git clone https://github.com/cyanheads/musicbrainz-mcp-server.git
cd musicbrainz-mcp-server
bun install
cp .env.example .env
```
- **Build and run:**
```sh
# One-time build
bun run rebuild
# Run the built server
bun run start:stdio
# or
bun run start:http
```
- **Run checks and tests:**
```sh
bun run devcheck # Lint, format, typecheck, security, packaging
bun run test # Vitest test suite
bun run lint:mcp # Validate MCP definitions against the linter rules
```
### Docker
```sh
docker build -t musicbrainz-mcp-server .
docker run --rm -e MUSICBRAINZ_CONTACT=you@example.com -p 3010:3010 musicbrainz-mcp-server
```
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `/var/log/musicbrainz-mcp-server`. OpenTelemetry peer dependencies are installed by default — build with `--build-arg OTEL_ENABLED=false` to omit them.
## Project structure
| Directory | Purpose |
|:---|:---|
| `src/index.ts` | `createApp()` entry point — registers tools and the resource, inits both services. |
| `src/config` | Server-specific environment variable parsing and validation with Zod. |
| `src/services/musicbrainz` | MusicBrainz WS/2 client — User-Agent, rate limiter, response cache, retry, domain types. |
| `src/services/cover-art` | Cover Art Archive client — maps a 404 to an empty image set, follows the release-group redirect. |
| `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). Ten read-only tools across search, lookup, browse, and cover art. |
| `src/mcp-server/resources` | Resource definitions. The `musicbrainz://{entity_type}/{mbid}` entity mirror. |
| `skills/` | Published agent guide and query reference, also served through MCP skill resources. |
| `tests/` | Unit and integration tests mirroring `src/`. |
## Development guide
See [`CLAUDE.md`/`AGENTS.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no `try/catch` in tool logic
- Use `ctx.log` for request-scoped logging, `ctx.state` for tenant-scoped storage
- Register new tools and resources via the barrels in `src/mcp-server/*/definitions/index.ts`
- Route every upstream call through the services, never a direct `fetch()`, so the User-Agent, rate limiter, and cache apply
- Wrap external API data: validate raw → normalize to domain type → return output schema; never fabricate missing fields
## Contributing
Issues are welcome. Run checks and tests before submitting:
```sh
bun run devcheck
bun run test
```
## License
Apache-2.0 — see [LICENSE](./LICENSE) for details.
MusicBrainz core entity data is [CC0](https://musicbrainz.org/doc/About/Data_License). The server reads only that core metadata and never fetches annotation text, which carries a different license. Cover art comes from the [Cover Art Archive](https://coverartarchive.org/), a joint project of MusicBrainz and the Internet Archive; image URLs are linked, never rehosted, and each image's copyright stays with its rights holders. Cite MusicBrainz and the Cover Art Archive in downstream use.
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessNo recent community issues