Metallum MCP
# Metallum MCP
An MCP server that lets Claude (or any MCP client) look things up in [Encyclopaedia Metallum (metal-archives.com)](https://www.metal-archives.com): bands, lineups over time, discographies, releases, tracklists, lyrics, artists, labels, reviews, similar bands and catalog browsing.
It is built on [pymetal](https://github.com/LeMetadatarr/pymetal), wrapped with a rate-limited client and a few extra fetchers for things pymetal doesn't cover.
## Tools (22)
**Search**
| Tool | What it does |
|---|---|
| `search_bands` | By name and/or advanced filters: genre, country, formation year range, lyrical themes, location, label. Pages with `offset`; returns `total` and `next_offset` |
| `search_albums` | By title, band, year range, release type, genre, label, country. Pages like `search_bands` |
| `search_songs` | By title, band, release, or **words in the lyrics** |
**Bands**
| Tool | What it does |
|---|---|
| `get_band` | Profile plus lineup grouped by current / past / live / last-known / guest, with each member's roles and years |
| `get_band_bio` | Full biography (the band page only shows an excerpt) |
| `get_discography` | Releases with type, year and review stats, optionally filtered by type, year range, or reviewed releases only |
| `band_review_stats` | For up to 25 bands at once: release count, review count and review-weighted average score, sorted best first. Filters by type, year range and minimum reviews |
| `get_similar_bands` | User-voted similar artists, ranked by votes |
| `get_band_links` | Official site, Bandcamp, Spotify, socials, merch |
| `get_band_reviews` | Review list (score, reviewer, date, URL) |
| `random_band` | A random band, optionally from a genre bucket |
**Releases**
| Tool | What it does |
|---|---|
| `get_album` | Details, tracklist (per-disc, bonus/instrumental flags, `lyrics_id`), lineup split into band / guest / staff |
| `get_other_versions` | Reissues, remasters and regional editions |
| `get_lyrics` | Lyrics by `lyrics_id` |
| `get_review` | Full text of one review |
**People and labels**
| Tool | What it does |
|---|---|
| `get_artist` | Real name, born/died, cause of death, origin, plus full biography and trivia |
| `get_label` | Address, styles, founding date, sub-labels, parent label |
**Browse and discovery**
| Tool | What it does |
|---|---|
| `browse_bands` | By country code, genre bucket or first letter |
| `browse_reviews` | Reviews posted in a given month |
| `get_upcoming_releases` | Upcoming releases, soonest first |
| `get_rip_artists` | The R.I.P. list |
| `list_countries` | Country codes for filters |
Typical flow: `search_bands` → `get_band` / `get_discography` → `get_album` → `get_lyrics`.
For "which bands are rated highest" questions: page through `search_bands`, then pass batches of ids to `band_review_stats`. Averages come from Metal Archives' rounded per-release averages, so treat scores within about half a point as ties.
## Setup
Requires [uv](https://docs.astral.sh/uv/). It installs Python 3.12 for the project; your system Python is not used.
```bash
git clone https://github.com/vincejyr/metallum-mcp.git
cd metallum-mcp
uv sync # create .venv and install pinned deps
uv run python test/smoke.py # end-to-end test against the live site (~75s)
```
### Claude Code
```bash
claude mcp add --scope user metallum -- "$(pwd)/.venv/bin/metallum-mcp"
```
### Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS), using the absolute path to your clone:
```json
{
"mcpServers": {
"metallum": {
"command": "/absolute/path/to/metallum-mcp/.venv/bin/metallum-mcp"
}
}
}
```
## How it uses pymetal
- **Pinned** to commit `ce5d75a` (v1.2.0a1, reviewed 2026-10-01). PyPI's `pymetal` is an older release published from a different repo, so install from git.
- **No anti-bot layer.** The `antibot` extra (`unblock_requests`: Cloudflare challenge solving, Wayback Machine fallback) is deliberately not installed. Plain requests work fine.
- **Polite client** (`src/metallum_mcp/client.py`) replaces pymetal's HTTP behaviour:
- It doesn't impersonate Chrome's TLS fingerprint, and sends a fixed, honest User-Agent instead of a random one on every request.
- It enforces the site's `Crawl-delay: 3` across all requests and threads.
- It caches responses in memory for 30 minutes, and discography pages on disk for 7 days (SQLite at `~/.cache/metallum-mcp/`), so review-stat scans survive restarts and don't re-crawl.
- **Browse and review tools fetch one page only** (`paginate=False`). pymetal's default is to walk the entire catalogue.
### Gaps in pymetal that this server fills (`extras.py`)
| Gap | Fix |
|---|---|
| `Band.comment` is always `None` (wrong HTML selector) | `get_band_bio` reads the site's "read more" endpoint |
| `Artist.biography` is always `None` (bio is loaded separately) | `get_artist` fetches the bio and trivia from the "read more" endpoints |
| No way to get a review's text | `get_review` parses the review page |
| With a country filter, `search_bands` puts the band's location in `country` | `search_bands` moves it to `location` and fills `country` from the filter |
These are worth reporting upstream.
## Settings
| Variable | Default | Purpose |
|---|---|---|
| `MA_MIN_INTERVAL_MS` | `3000` | Minimum gap between requests |
| `MA_CACHE_TTL_MS` | `1800000` | In-memory cache lifetime |
| `MA_DISK_CACHE_TTL_DAYS` | `7` | Disk cache lifetime for discographies (`0` disables it) |
| `MA_CACHE_DIR` | `~/.cache/metallum-mcp` | Disk cache location |
| `MA_USER_AGENT` | `Mozilla/5.0 (compatible; metallum-mcp/1.0; personal use)` | User-Agent header |
## Notes
- Each uncached request takes about 3 seconds. `get_artist` with its bio makes 3 requests, and a genre-filtered `random_band` can take up to about 30 seconds. `band_review_stats` makes one request per uncached band, so a full batch of 25 takes about 75 seconds the first time.
- Meant for personal, interactive lookups, not bulk scraping.
- Parsing depends on the site's HTML. If a tool starts returning empty fields, run the smoke test to see which one broke.
- Uses MCP Python SDK 2.x (`MCPServer`, formerly `FastMCP`).
## Evals
`evals/` holds an end-to-end eval: 16 real research questions (facts, multi-step lookups, rating rankings, name collisions, edge cases, an ambiguous question), run through headless Claude Code with only this server attached. Graders check the model's final `ANSWER:` line. One ambiguity case is judged by Sonnet 5.5. Each case also records tool calls, whether `band_review_stats` was used, tokens, cost and latency. Cases and expected answers are in `evals/cases.md`.
Site data is replayed from recorded fixtures (`MA_FIXTURES` + `MA_FIXTURE_MODE=record|replay` in the client), so scores don't drift as Metal Archives changes and runs don't crawl the site. The fixtures aren't committed. Record your own first (about 4 minutes, live site). This writes to a scratch flow, because the committed baseline's results would make the runner skip every case:
```bash
EVAL_FIXTURE_MODE=record node evals/run-eval.mjs --flow .claude/hillclimb/metallum-record --variant baseline --model claude-opus-5-5 --concurrency 1 --approve-harness
```
After that, runs replay offline (`EVAL_FIXTURE_MODE=replay`, the default), e.g. `--flow .claude/hillclimb/metallum-research --variant v1` to compare a change against the committed baseline. A page that wasn't recorded fails its case as `fixture_miss` rather than being fetched. Re-run in record mode to fill it in. `--approve-harness` records a fingerprint of the runner and cases; the runner refuses to run if they change until someone approves again.
Baseline (2026-10-02, Opus 5.5): 16/16 correct, ~$0.76 per full run, median 2 tool calls and ~8 s per case.
## Layout
```
src/metallum_mcp/server.py MCP server and tool definitions
src/metallum_mcp/client.py rate-limited, cached HTTP client for pymetal
src/metallum_mcp/extras.py bios, trivia, review text
test/smoke.py end-to-end test through a real MCP client
evals/ eval runner, cases and grading (see Evals)
```
## Credits and disclaimer
- Data comes from [Encyclopaedia Metallum](https://www.metal-archives.com), maintained by its volunteer community. This project is not affiliated with or endorsed by Metal Archives. Please respect the site and its rate limits.
- Built on [pymetal](https://github.com/LeMetadatarr/pymetal) (Apache-2.0).
## License
MIT. See [LICENSE](LICENSE).
TDQS
Scored across 21 tools
Most tools have clearly distinct purposes, helped by descriptions that cross-reference each other (e.g. get_band vs get_band_bio, get_band_reviews vs browse_reviews). Mild overlap exists between browse_bands and search_bands, and the many band-centric getters could briefly confuse, but each targets a different facet.
Nearly all tools follow a consistent snake_case verb_noun pattern (get_, search_, browse_, list_). Only random_band deviates by omitting a verb, a minor inconsistency in an otherwise predictable scheme.
21 tools is on the high side for the typical 3-15 range, but the breadth of the Metallum domain (artists, bands, albums, songs, lyrics, reviews, labels, upcoming, RIP) justifies most of them. Each tool appears to cover a distinct retrieval need, so the count is slightly heavy but reasonable.
The read-only surface is comprehensive: artists, bands, albums, songs, lyrics, reviews, labels, countries, upcoming releases, and RIP lists are all covered. Minor gaps include no dedicated search_artists or search_labels, and get_artist does not explicitly return associated bands, but these are workable omissions.