mcp-lrclib
This server is a read-only MCP interface to LRCLIB that lets you search for music tracks and fetch their lyrics — including time-synced (LRC) karaoke-style lyrics. No API key or account is required.
Search for tracks (
search_tracks): Find songs by free-text query or structured fields (title, artist, album). Returns metadata including track ID, artist, album, duration, and flags indicating whether plain or time-synced lyrics are available. Lyrics are stripped from search results for efficiency.Fetch lyrics (
get_lyrics): Retrieve lyrics for a specific track by its ID (from search) or by exact artist/title. Supports three formats:plain— plain text lyrics onlysynced— raw LRC text plus a parsed list of{time_seconds, text}entries for karaoke or audio syncboth— plain and synced togetherHandles pagination via
offsetandmax_charsfor long lyricsReturns
status: "instrumental"for tracks with no vocals (a valid answer, not an error) andstatus: "no_lyrics"when lyrics are unavailable
Get track metadata (
get_track): Look up a single track by ID and retrieve its full metadata (title, artist, album, formatted duration, lyrics availability flags) without fetching lyrics — useful for confirming the right version before fetching.Disambiguate versions: Pass
duration_secondsandalbum_name(or use the ID from search) to pick the correct release or re-recording of a song.
Key characteristics:
Read-only: Never writes or contributes data back to LRCLIB
No configuration required: Works out of the box with
npx -y mcp-lrclibBuilt-in caching and rate limiting: Responses are cached in-memory (15 min default); requests are paced to respect LRCLIB's API
Metadata search only: Cannot search by words found inside lyrics — only by track/artist/album metadata
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-lrclibget the synced lyrics for 'Bohemian Rhapsody' by Queen"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-lrclib
LRCLIB is a free, open database of song lyrics, built by the people who use it and offered to anyone without a key or an account. It holds two forms of the words: the plain text of a song, and the LRC form, where every line carries the moment it is sung, which is what a karaoke display or a lyrics panel follows along with. A track is filed there by its title, its artist, its album and its duration, so the several releases of one song sit side by side.
This server connects a chat client to that database. You can search for a track by title, artist or album, read the plain words of a song, read its time-synced lines with their timestamps, and check the metadata of one release before reading it. It needs no API key and no account.
Install
One-click install
Claude Code
claude mcp add lrclib -- npx -y mcp-lrclibClaude Desktop, Cursor, and any client using the standard config format
{
"mcpServers": {
"lrclib": {
"command": "npx",
"args": ["-y", "mcp-lrclib"]
}
}
}Node 24 or later is required, and no environment variable has to be set.
With Docker
{
"mcpServers": {
"lrclib": {
"command": "docker",
"args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-lrclib:2.0.1"]
}
}
}-i keeps stdin open, which is where the protocol travels, and -t is left out
because a TTY rewrites the stream. The container needs outbound HTTPS to
lrclib.net, and nothing else: no volume, no port, no credential.
Bundle, without npm
Download mcp-lrclib-2.0.1.mcpb from
the latest release and
open it. A client that supports MCP bundles installs it on its own, with no npm
and no configuration file to edit. The bundle carries its dependencies, so
nothing is fetched at install time.
Related MCP server: LyricsGenius MCP Server
What you can ask
"Find me the lyrics of Le Sud by Nino Ferrer."
"Give me the timed lyrics of Bohemian Rhapsody so I can follow along."
"Which version of Hallelujah is on LRCLIB, and how long is each one?"
"Read me the second half of those lyrics."
"Does track 3396226 have synced lyrics?"
The ordinary path runs from a search to a reading: search_tracks names an id,
and get_lyrics takes that id.
Tools
Tool | What it does |
| Finds tracks by title, artist or album, with their metadata. |
| Reads the words of one track, plain or with their timestamps. |
| Reads the metadata of one track by its id, without the words. |
LRCLIB files a track by its metadata, so a search reaches a song through its title, its artist or its album. A word remembered from inside a song finds nothing there.
search_tracks
Finds the tracks whose metadata matches, in one free-text search or in fields of their own. Several releases of one song come back side by side, and their album and duration tell them apart.
Argument | Type | Required | What it does |
| string, 1 to 200 characters | no | Free-text search, as in |
| string, up to 200 characters | no | Song title, for a search by field. |
| string, up to 200 characters | no | Artist name, for a search by field. |
| string, up to 200 characters | no | Album name, to narrow a search by field. |
| integer, 1 to 50, default | no | Rows to serve. LRCLIB answers up to 20 per search. |
Pass query, or one of the three fields.
In return: rows carrying id, which get_lyrics and get_track take;
track_name and artist_name; album_name and duration_seconds, which tell
two releases of one song apart; instrumental; has_plain_lyrics and
has_synced_lyrics, so timed lines can be checked for before they are asked
for; and source_url. Alongside come result_count and total_available, the
tracks LRCLIB served before limit was applied. album_name and
duration_seconds are null on a track filed without them, and the rows carry
no words at all: get_lyrics reads those.
get_lyrics
Reads the words of one track, either as plain text or as LRC lines carrying the moment each one is sung. Long lyrics are served a slice at a time, resuming at a line boundary.
Argument | Type | Required | What it does |
| integer, positive | no | The LRCLIB track id, as |
| string, up to 200 characters | no | Artist name, matched exactly. Needed when |
| string, up to 200 characters | no | Song title, matched exactly. Needed when |
| string, up to 200 characters | no | Album name, to pick between releases. |
| number, positive | no | Track duration, to pick between versions of differing length. |
|
| no | Which form of the words to serve. |
| integer, 200 to 20000, default | no | Characters of text to serve in this call. |
| integer, 0 or more, default | no | Character offset to resume from. |
In return: status, which reads ok, instrumental for a track with no
words to sing, or no_lyrics for one filed without them; track with its id,
title, artist, album, duration and source_url; plain_lyrics; synced_lyrics
as raw LRC text and synced_lines as a list of { time_seconds, text }, with
synced_lines_truncated when the slice cut them. The reading is described by
paginated_form, total_chars, returned_chars, offset, next_offset and
truncated: pass next_offset back to read on, and null there means the end.
attribution is the line to cite when the words are shown. A status of
instrumental is a complete answer.
get_track
Reads the metadata of one track from its id, leaving the words aside. It confirms a release before the words are asked for, and it resolves an id carried over from earlier in a conversation.
Argument | Type | Required | What it does |
| integer, positive | yes | The LRCLIB track id, as |
In return: track, holding the fields a search row carries, and
duration_formatted as m:ss, which is null when the track is filed without a
duration. has_plain_lyrics and has_synced_lyrics say which forms
get_lyrics can serve for it.
Configuration
Every variable is optional. Set them in the env block of your client config.
Variable | Default | What it does |
| the project identity | Names your application to LRCLIB, with an address where a person can be reached. |
|
| Gap between two requests, from 200 to 60000. A figure under the floor is refused and this one used. |
|
| Deadline for one request, from 1000 to 120000. |
|
| Attempts after a transient failure, from 0 to 10. |
|
| How long an answer stays in memory, from 0 to 86400000. |
|
| Answers held in memory at once, from 0 to 10000. |
|
|
|
A value outside its range falls back to the default, and the reason is written to stderr.
Errors
Every failure carries one of six codes, a message, and where it helps a hint naming the next move.
Code | What happened | What to do |
| LRCLIB answered, and it holds no such track. | Check the spelling with |
| The arguments were refused before any request went out. | Read the message, which names the argument. |
| LRCLIB asked this client to slow down. | Wait the number of seconds the hint names and call again with the same arguments. The track is still there. |
| LRCLIB answered in a shape this client cannot read. | Report it at the issue tracker. |
| The request did not complete. | Try again shortly. |
| The request passed its deadline. | Raise |
As a library
The layer reading LRCLIB is published on its own, with its pacing, its cache and its errors, and with no protocol attached.
import { LrclibClient } from "mcp-lrclib/client";
const client = new LrclibClient();
const { data, cached } = await client.getById(3396226);
console.log(data.track_name, data.synced_lyrics !== null, cached);search, get and getById each answer { data, cached }, and throw an error
carrying one of the six codes. The floor between two requests holds here as well.
Pacing and attribution
Requests go out one at a time with a minimum gap between them, and that floor
holds however the server is configured. The User-Agent always ends with the
project identity and an address where a person can be reached. LRCLIB is a free
service and publishes its API for machines to read, and this server reads it on
demand, one call at a time, in answer to something you asked for.
Every result carries the artist, the title and the address of its LRCLIB page,
and get_lyrics carries attribution, the three of them written as one line.
Song lyrics are the work of their authors and publishers. This project claims no rights over them, ships no database of them, writes nothing to disk, and contributes nothing back to LRCLIB. This MCP server is an unofficial project, with no affiliation to LRCLIB.
Privacy
This server collects nothing about you and sends nothing to its author. It runs
on your machine, contacts lrclib.net and nothing else, holds its answers in memory
while it runs, and writes nothing to disk.
PRIVACY.md states what a request carries and which settings change
any of it.
Development
npm install
npm run build:fixtures
npm test
npm run checkTests run against generated fixtures and make no network request. The live suite,
npm run test:live, makes one request per route and runs nightly against the
service itself.
Contributing
Bugs, questions and ideas belong in the issue tracker. Pull requests are welcome; opening an issue first helps agree on the shape of the change. See CONTRIBUTING.md.
License
MIT, see LICENSE. The lyrics belong to their authors and publishers, and the database to LRCLIB and its contributors.
mcp-lrclib (français)
LRCLIB est une base de paroles de chansons libre et ouverte, alimentée par ceux qui s'en servent et offerte à tous sans clé ni compte. Elle contient deux formes des paroles : le texte simple d'une chanson, et la forme LRC, où chaque ligne porte le moment où elle est chantée, ce que suit un affichage karaoké ou un panneau de paroles. Un titre y est classé par son nom, son artiste, son album et sa durée, si bien que les différentes parutions d'une même chanson y voisinent.
Ce serveur relie un client de conversation à cette base. On peut y chercher un titre par son nom, son artiste ou son album, lire les paroles simples d'une chanson, lire ses lignes horodatées avec leurs marques de temps, et vérifier la fiche d'une parution avant de la lire. Aucune clé d'API, aucun compte.
Installation
Installation en un clic
Claude Code
claude mcp add lrclib -- npx -y mcp-lrclibClaude Desktop, Cursor, et tout client au format de configuration standard
{
"mcpServers": {
"lrclib": {
"command": "npx",
"args": ["-y", "mcp-lrclib"]
}
}
}Node 24 ou plus récent est nécessaire, et aucune variable d'environnement n'est à renseigner.
Avec Docker
{
"mcpServers": {
"lrclib": {
"command": "docker",
"args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-lrclib:2.0.1"]
}
}
}-i garde l'entrée standard ouverte, qui est le canal du protocole, et -t est
omis parce qu'un TTY réécrit le flux. Le conteneur a besoin d'un accès HTTPS
sortant vers lrclib.net, et de rien d'autre : aucun volume, aucun port, aucun
identifiant.
Bundle, sans npm
Téléchargez mcp-lrclib-2.0.1.mcpb depuis
la dernière publication
et ouvrez-le. Un client qui gère les bundles MCP l'installe seul, sans npm et
sans fichier de configuration à modifier. Le bundle emporte ses dépendances, donc
rien n'est téléchargé à l'installation.
Ce qu'on peut demander
« Trouve-moi les paroles du Sud de Nino Ferrer. »
« Donne-moi les paroles horodatées de Bohemian Rhapsody pour que je suive. »
« Quelles versions de Hallelujah y a-t-il sur LRCLIB, et quelle est leur durée ? »
« Lis-moi la seconde moitié de ces paroles. »
« Est-ce que le titre 3396226 a des paroles synchronisées ? »
Le chemin ordinaire va d'une recherche à une lecture : search_tracks nomme un
id, et get_lyrics reprend cet identifiant.
Les outils
Outil | Ce qu'il fait |
| Trouve des titres par nom, artiste ou album, avec leurs fiches. |
| Lit les paroles d'un titre, simples ou horodatées. |
| Lit la fiche d'un titre par son identifiant, sans les paroles. |
LRCLIB classe un titre par sa fiche, donc une recherche atteint une chanson par son nom, son artiste ou son album. Un mot retenu de l'intérieur d'une chanson n'y trouve rien.
search_tracks
Trouve les titres dont la fiche correspond, en une recherche libre ou par champs. Plusieurs parutions d'une même chanson reviennent côte à côte, et leur album et leur durée les distinguent.
Argument | Type | Requis | Ce qu'il fait |
| chaîne, 1 à 200 caractères | non | Recherche libre, par exemple |
| chaîne, jusqu'à 200 caractères | non | Nom de la chanson, pour une recherche par champs. |
| chaîne, jusqu'à 200 caractères | non | Nom de l'artiste, pour une recherche par champs. |
| chaîne, jusqu'à 200 caractères | non | Nom de l'album, pour resserrer une recherche par champs. |
| entier, 1 à 50, défaut | non | Lignes à servir. LRCLIB en rend jusqu'à 20 par recherche. |
Passez query, ou l'un des trois champs.
En retour : des lignes portant id, que get_lyrics et get_track
reprennent ; track_name et artist_name ; album_name et duration_seconds,
qui distinguent deux parutions d'une même chanson ; instrumental ;
has_plain_lyrics et has_synced_lyrics, qui permettent de vérifier l'existence
des lignes horodatées avant de les demander ; et source_url. Viennent aussi
result_count et total_available, les titres que LRCLIB a servis avant
l'application de limit. album_name et duration_seconds valent null sur un
titre classé sans eux, et les lignes ne portent aucune parole : get_lyrics les
lit.
get_lyrics
Lit les paroles d'un titre, en texte simple ou en lignes LRC portant le moment où chacune est chantée. Des paroles longues sont servies par tranches, coupées sur une fin de ligne.
Argument | Type | Requis | Ce qu'il fait |
| entier, positif | non | L'identifiant LRCLIB rendu par |
| chaîne, jusqu'à 200 caractères | non | Nom de l'artiste, correspondance exacte. Nécessaire sans |
| chaîne, jusqu'à 200 caractères | non | Nom de la chanson, correspondance exacte. Nécessaire sans |
| chaîne, jusqu'à 200 caractères | non | Nom de l'album, pour choisir entre des parutions. |
| nombre, positif | non | Durée du titre, pour choisir entre des versions de longueurs différentes. |
|
| non | La forme des paroles à servir. |
| entier, 200 à 20000, défaut | non | Caractères de texte à servir dans cet appel. |
| entier, 0 ou plus, défaut | non | Position en caractères où reprendre. |
En retour : status, qui vaut ok, instrumental pour un titre sans
paroles à chanter, ou no_lyrics pour un titre classé sans elles ; track avec
son identifiant, son nom, son artiste, son album, sa durée et son source_url ;
plain_lyrics ; synced_lyrics en texte LRC brut et synced_lines en liste de
{ time_seconds, text }, avec synced_lines_truncated quand la tranche les a
coupées. La lecture est décrite par paginated_form, total_chars,
returned_chars, offset, next_offset et truncated : redonnez next_offset
pour poursuivre, et null marque la fin. attribution est la ligne à citer
quand les paroles sont montrées. Un status à instrumental est une réponse
complète.
get_track
Lit la fiche d'un titre depuis son identifiant, sans les paroles. Elle confirme une parution avant qu'on demande les paroles, et elle résout un identifiant venu d'un échange précédent.
Argument | Type | Requis | Ce qu'il fait |
| entier, positif | oui | L'identifiant LRCLIB rendu par |
En retour : track, qui porte les champs d'une ligne de recherche, et
duration_formatted sous la forme m:ss, null pour un titre classé sans
durée. has_plain_lyrics et has_synced_lyrics disent quelles formes
get_lyrics peut servir.
Configuration
Chaque variable est facultative. Elles se posent dans le bloc env de la
configuration du client.
Variable | Défaut | Ce qu'elle fait |
| l'identité du projet | Nomme votre application auprès de LRCLIB, avec une adresse où joindre une personne. |
|
| Écart entre deux requêtes, de 200 à 60000. Une valeur sous le plancher est refusée au profit de celle-ci. |
|
| Délai d'une requête, de 1000 à 120000. |
|
| Tentatives après un échec passager, de 0 à 10. |
|
| Durée pendant laquelle une réponse reste en mémoire, de 0 à 86400000. |
|
| Réponses gardées en mémoire à la fois, de 0 à 10000. |
|
|
|
Une valeur hors de sa plage retombe sur le défaut, et la raison est écrite sur la sortie d'erreur.
Erreurs
Chaque échec porte un des six codes, un message, et quand cela aide une indication du geste suivant.
Code | Ce qui s'est passé | Que faire |
| LRCLIB a répondu, et ne contient pas ce titre. | Vérifiez l'orthographe avec |
| Les arguments ont été refusés avant toute requête. | Lisez le message, qui nomme l'argument. |
| LRCLIB demande à ce client de ralentir. | Attendez les secondes indiquées et rappelez avec les mêmes arguments. Le titre est toujours là. |
| LRCLIB a répondu dans une forme que ce client ne lit pas. | Signalez-le sur le suivi d'incidents. |
| La requête n'a pas abouti. | Réessayez sous peu. |
| La requête a dépassé son délai. | Augmentez |
Comme bibliothèque
La couche qui lit LRCLIB est publiée seule, avec son rythme, son cache et ses erreurs, sans protocole attaché.
import { LrclibClient } from "mcp-lrclib/client";
const client = new LrclibClient();
const { data, cached } = await client.getById(3396226);
console.log(data.track_name, data.synced_lyrics !== null, cached);search, get et getById répondent chacun { data, cached }, et lèvent une
erreur portant un des six codes. Le plancher entre deux requêtes tient également
ici.
Rythme et attribution
Les requêtes partent une à une avec un écart minimal entre elles, et ce plancher
tient quelle que soit la configuration. Le User-Agent se termine toujours par
l'identité du projet et une adresse où joindre une personne. LRCLIB est un
service gratuit et publie son API pour être lue par des machines, et ce serveur
la lit à la demande, un appel à la fois, en réponse à ce que vous avez demandé.
Chaque résultat porte l'artiste, le titre et l'adresse de sa page LRCLIB, et
get_lyrics porte attribution, ces trois éléments écrits en une ligne.
Les paroles sont l'œuvre de leurs auteurs et de leurs éditeurs. Ce projet ne revendique aucun droit dessus, n'embarque aucune base de paroles, n'écrit rien sur le disque et ne contribue rien à LRCLIB. Ce MCP est un projet non officiel, sans affiliation à LRCLIB.
Confidentialité
Ce serveur ne collecte rien sur vous et n'envoie rien à son auteur. Il tourne sur
votre machine, ne joint que lrclib.net, garde ses réponses en mémoire le temps qu'il
tourne, et n'écrit rien sur le disque. PRIVACY.md dit ce qu'une
requête emporte et quels réglages changent cela.
Développement
npm install
npm run build:fixtures
npm test
npm run checkLes tests s'exécutent sur des fixtures engendrées et n'émettent aucune requête.
La suite en direct, npm run test:live, émet une requête par route et tourne
chaque nuit contre le service lui-même.
Contribuer
Les anomalies, les questions et les idées ont leur place dans le suivi d'incidents. Les propositions de modification sont bienvenues ; ouvrir un ticket d'abord aide à s'accorder sur la forme du changement. Voir CONTRIBUTING.md.
Licence
MIT, voir LICENSE. Les paroles appartiennent à leurs auteurs et à leurs éditeurs, et la base à LRCLIB et à ses contributeurs.
Available Tools
3 toolsget_lyricsGet lyricsARead-onlyIdempotent
Fetch the lyrics of one track from LRCLIB, given the id returned by search_tracks, or an exact artist and title. Set 'format' to "synced" for karaoke-style lyrics with a timestamp on every line, "plain" for the text alone, or "both". Check 'has_synced_lyrics' in the search results first: not every track has timed lyrics. Lyrics can be long, so the text is truncated by default: check 'truncated' and call again with 'offset' set to 'next_offset' to continue reading. Instrumental tracks come back with status "instrumental" and no text, which is a valid answer, so do not retry them. Always cite 'attribution' when showing lyrics to a user.
| Name | Required | Description | Default |
|---|---|---|---|
| id | No | LRCLIB track id, as returned by search_tracks. Preferred over the name fields. | |
| format | No | 'plain' returns the text only. 'synced' returns LRC text plus a parsed list of timestamped lines. 'both' returns each of them. | plain |
| offset | No | Character offset to resume from, for lyrics longer than max_chars. | |
| max_chars | No | Maximum characters of lyrics text to return in this call. | |
| album_name | No | Album name, to pick between releases. | |
| track_name | No | Song title, matched exactly by LRCLIB. Required when 'id' is absent. | |
| artist_name | No | Artist name, matched exactly by LRCLIB. Required when 'id' is absent. | |
| duration_seconds | No | Track duration, to pick between versions of differing length. |
Output Schema
| Name | Required | Description |
|---|---|---|
| notes | Yes | |
| track | Yes | |
| offset | Yes | |
| source | Yes | |
| status | Yes | |
| truncated | Yes | |
| attribution | Yes | |
| next_offset | Yes | |
| total_chars | Yes | |
| plain_lyrics | Yes | |
| synced_lines | Yes | Parsed timestamped lines. |
| synced_lyrics | Yes | Raw LRC text, one timestamp per line. |
| paginated_form | Yes | Which body the character counts and the offset describe. Timed text carries its timestamps, so it is longer than the words alone. |
| returned_chars | Yes | |
| synced_lines_truncated | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already establish readOnly, idempotent, and non-destructive behavior. The description adds valuable details beyond annotations: default truncation with 'truncated'/'next_offset', instrumental status as a valid no-retry outcome, and the attribution requirement. No contradiction with annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Five dense sentences, each carrying distinct operational information. Purpose is front-loaded, and there is no filler or redundant restating of schema content.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Despite a complex 8-parameter surface, the description covers all critical runtime behaviors: input selection, format choice, synced availability checking, truncation/continuation, the instrumental edge case, and attribution. The output schema handles return-value details, so nothing essential is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so all parameters are already documented. The description goes further by linking id to search_tracks results, clarifying artist/track exact-match semantics as a fallback, explaining format values in user terms, and describing how offset and max_chars interact with truncation flags.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a clear verb and resource: 'Fetch the lyrics of one track from LRCLIB.' It distinguishes itself from siblings by referencing the id from search_tracks and exact artist/title fallback, making its scope unmistakable.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides strong operational context: how to identify the track, when to expect synced lyrics via has_synced_lyrics, and how to handle instrumental tracks as valid results. It does not explicitly name get_track as the metadata alternative, but the workflow guidance is clear and actionable.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_trackGet track metadataARead-onlyIdempotent
Look up one LRCLIB track by its id and return its metadata without the lyrics: title, artist, album, duration, and whether plain and time-synced lyrics exist. Use this to confirm you have the right release or version before calling get_lyrics, or to check whether timed lyrics exist for a track id you already hold.
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | LRCLIB track id, as returned by search_tracks. |
Output Schema
| Name | Required | Description |
|---|---|---|
| notes | Yes | |
| track | Yes | |
| source | Yes | |
| duration_formatted | Yes | Duration as m:ss, when known. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, and non-destructive behavior, so the description is not required to repeat those. The description adds meaningful behavioral context by specifying the return excludes lyrics and includes existence flags for plain and time-synced lyrics, which is not derivable from the schema or annotations alone.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences with no filler. The first sentence states the core action and return payload; the second provides focused usage guidance. Everything present earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The tool is simple, has a single fully documented parameter, rich annotations, and an output schema. The description adds the missing context around when to use the tool and what it returns, making the complete call scenario clear. Nothing necessary for correct invocation is omitted.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%: the id parameter is already described as 'LRCLIB track id, as returned by search_tracks.' The description does not need to add further parameter detail, so the baseline of 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb 'Look up' with a clear resource ('one LRCLIB track by its id') and lists the returned fields, distinguishing this from get_lyrics by explicitly stating 'without the lyrics'. This makes the tool's function unambiguous and differentiates it from both siblings.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly states when to use this tool: 'confirm you have the right release or version before calling get_lyrics' and 'check whether timed lyrics exist for a track id you already hold.' It references the closest sibling (get_lyrics) as the next step, providing clear routing guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_tracksSearch tracksARead-onlyIdempotent
Search LRCLIB for tracks by title, artist or album, and get the id needed to fetch lyrics with get_lyrics. Give either 'query' as free text, or 'track_name' and 'artist_name' separately for a narrower search. Results carry metadata only: title, artist, album, duration, and whether plain and time-synced lyrics exist. Use 'duration_seconds' and 'album_name' in the results to tell releases and re-recordings of the same song apart, and 'has_synced_lyrics' to know whether karaoke-style timed lyrics are available before fetching them. LRCLIB indexes tracks by their metadata, so it cannot search for a word appearing inside the lyrics.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Maximum tracks to return. LRCLIB returns up to 20 per search. | |
| query | No | Free-text search, for example 'nino ferrer le sud'. Use this or the fields below. | |
| album_name | No | Album name, to narrow a structured search. | |
| track_name | No | Song title, for a structured search. | |
| artist_name | No | Artist name, for a structured search. |
Output Schema
| Name | Required | Description |
|---|---|---|
| notes | Yes | |
| source | Yes | |
| results | Yes | |
| result_count | Yes | |
| total_available | Yes | Tracks LRCLIB returned before applying 'limit'. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already mark the tool as read-only, idempotent, and non-destructive, and the description adds meaningful behavioral context beyond that: results are metadata-only, tracks are indexed by metadata rather than lyric content, and has_synced_lyrics indicates whether timed lyrics exist before fetching. This is exactly the kind of non-obvious behavior an agent needs to know.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded with the core purpose, then parameter guidance, then result interpretation, then a key limitation. Each sentence earns its place and none are filler, making it informative without being bloated.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the annotations, output schema, and sibling tools, the description is complete enough to select and invoke the tool correctly. It explains the two search modes, the metadata-only nature of results, how to disambiguate releases, how to check for synced lyrics, and the limitation around lyric-text search. The only minor gap is not addressing get_track directly, but the overall context is still strong.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds value by explaining the relationship between parameters: 'query' is free text, while 'track_name' and 'artist_name' provide a narrower structured search. It also explains how result fields like duration_seconds and album_name can be used for disambiguation, going slightly beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource: 'Search LRCLIB for tracks by title, artist or album', and clarifies that the purpose is to obtain 'the id needed to fetch lyrics with get_lyrics'. This clearly differentiates it from the sibling tools by positioning search as the discovery step before retrieval.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives solid usage guidance: use 'query' for free text or 'track_name' and 'artist_name' for a narrower search, and it explicitly warns that LRCLIB cannot search lyric content. It names get_lyrics as the downstream consumer, though it does not explicitly contrast search_tracks with get_track or state when get_track would be preferred.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
3 tool updates
v2.0.0- Changed
get_lyrics19 fields changed- added
Input schema / properties / id / maximumAdded value: +9007199254740991 - added
Input schema / properties / offset / maximumAdded value: +9007199254740991 - changed
Output schema / properties / next_offset / anyOfPrevious value: -[ - { - "type": "integer" - }, - { - "type": "null" - } -]New value: +[ + { + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + { + "type": "null" + } +] - added
Output schema / properties / offset / maximumAdded value: +9007199254740991 - added
Output schema / properties / offset / minimumAdded value: +-9007199254740991 - added
Output schema / properties / plain_lyrics / anyOfAdded value: +[ + { + "type": "string" + }, + { + "type": "null" + } +] - removed
Output schema / properties / plain_lyrics / typeRemoved value: -[ - "string", - "null" -] - added
Output schema / properties / returned_chars / maximumAdded value: +9007199254740991 - added
Output schema / properties / returned_chars / minimumAdded value: +-9007199254740991 - added
Output schema / properties / synced_lyrics / anyOfAdded value: +[ + { + "type": "string" + }, + { + "type": "null" + } +] - removed
Output schema / properties / synced_lyrics / typeRemoved value: -[ - "string", - "null" -] - added
Output schema / properties / total_chars / maximumAdded value: +9007199254740991 - added
Output schema / properties / total_chars / minimumAdded value: +-9007199254740991 - added
Output schema / properties / track / properties / album_name / anyOfAdded value: +[ + { + "type": "string" + }, + { + "type": "null" + } +] - removed
Output schema / properties / track / properties / album_name / typeRemoved value: -[ - "string", - "null" -] - added
Output schema / properties / track / properties / duration_seconds / anyOfAdded value: +[ + { + "type": "number" + }, + { + "type": "null" + } +] - removed
Output schema / properties / track / properties / duration_seconds / typeRemoved value: -[ - "number", - "null" -] - added
Output schema / properties / track / properties / id / maximumAdded value: +9007199254740991 - added
Output schema / properties / track / properties / id / minimumAdded value: +-9007199254740991
- Changed
get_track9 fields changed- added
Input schema / properties / id / maximumAdded value: +9007199254740991 - added
Output schema / properties / duration_formatted / anyOfAdded value: +[ + { + "type": "string" + }, + { + "type": "null" + } +] - removed
Output schema / properties / duration_formatted / typeRemoved value: -[ - "string", - "null" -] - added
Output schema / properties / track / properties / album_name / anyOfAdded value: +[ + { + "type": "string" + }, + { + "type": "null" + } +] - removed
Output schema / properties / track / properties / album_name / typeRemoved value: -[ - "string", - "null" -] - added
Output schema / properties / track / properties / duration_seconds / anyOfAdded value: +[ + { + "type": "number" + }, + { + "type": "null" + } +] - removed
Output schema / properties / track / properties / duration_seconds / typeRemoved value: -[ - "number", - "null" -] - added
Output schema / properties / track / properties / id / maximumAdded value: +9007199254740991 - added
Output schema / properties / track / properties / id / minimumAdded value: +-9007199254740991
- Changed
search_tracks10 fields changed- added
Output schema / properties / result_count / maximumAdded value: +9007199254740991 - added
Output schema / properties / result_count / minimumAdded value: +-9007199254740991 - added
Output schema / properties / results / items / properties / album_name / anyOfAdded value: +[ + { + "type": "string" + }, + { + "type": "null" + } +] - removed
Output schema / properties / results / items / properties / album_name / typeRemoved value: -[ - "string", - "null" -] - added
Output schema / properties / results / items / properties / duration_seconds / anyOfAdded value: +[ + { + "type": "number" + }, + { + "type": "null" + } +] - removed
Output schema / properties / results / items / properties / duration_seconds / typeRemoved value: -[ - "number", - "null" -] - added
Output schema / properties / results / items / properties / id / maximumAdded value: +9007199254740991 - added
Output schema / properties / results / items / properties / id / minimumAdded value: +-9007199254740991 - added
Output schema / properties / total_available / maximumAdded value: +9007199254740991 - added
Output schema / properties / total_available / minimumAdded value: +-9007199254740991
1 tool update
v1.4.0- Changed
get_lyrics2 fields changed- added
Output schema / properties / paginated_formAdded value: +{ + "description": "Which body the character counts and the offset describe. Timed text carries its timestamps, so it is longer than the words alone.", + "enum": [ + "plain", + "synced" + ], + "type": "string" +} - changed
Output schema / requiredPrevious value: -[ - "status", - "track", - "plain_lyrics", - "synced_lyrics", - "synced_lines", - "synced_lines_truncated", - "total_chars", - "returned_chars", - "offset", - "next_offset", - "truncated", - "attribution", - "source", - "notes" -]New value: +[ + "status", + "track", + "plain_lyrics", + "synced_lyrics", + "synced_lines", + "synced_lines_truncated", + "paginated_form", + "total_chars", + "returned_chars", + "offset", + "next_offset", + "truncated", + "attribution", + "source", + "notes" +]
3 tool updates
v1.0.0- First observed
get_lyrics - First observed
get_track - First observed
search_tracks
TDQS
Scored across 3 tools
The tools are mostly distinct: search_tracks finds tracks by query while get_track fetches a specific track by ID, and get_lyrics retrieves lyric content. There is minor overlap in that both search_tracks and get_track return metadata, but their purposes are clearly separated by search vs. direct lookup.
All tool names follow a consistent verb_noun pattern: search_tracks, get_lyrics, and get_track. The naming is predictable and makes the action and resource type immediately clear.
Three tools is an appropriate scope for an LRCLIB lyrics server. Each tool covers a distinct part of the workflow: searching, fetching lyrics, and fetching track metadata, with no unnecessary duplication.
The tool surface covers the full read-only workflow for this domain: discover tracks, inspect metadata, and fetch either plain or synced lyrics. Descriptions also handle edge cases like truncation, instrumental tracks, and timing checks, so there are no obvious dead ends.
Maintenance
Related MCP Connectors
MCP server for Suno AI music generation, lyrics, and covers
An MCP server that provides tools to discover and retrieve podcast episodes transcripts.
MCP server for RiverScript, an AI transcription platform - fetches transcripts shared via a link.
MCP server for Producer/Riffusion AI music generation
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceAn MCP server for Spotify control and synchronized lyrics retrieval that enables playback management, queue navigation, and music search capabilities. It also features perception tools for real-time track analysis, including BPM, key detection, and timestamped lyrics.1163Apache 2.0
- AlicenseNot gradedqualityDmaintenanceAn MCP server that enables searching Genius.com for artists, songs, and albums, retrieving song lyrics, and exploring artist discographies through natural language.3MIT
- AlicenseNot gradedqualityBmaintenanceBridges multiple lyrics providers (LRCLIB, Genius, Musixmatch, Melon) into a single toolchain accessible via MCP, HTTP, or CLI.741MIT
- AlicenseNot gradedqualityAmaintenanceMCP server that enables LLMs to search, play, and manage music from multiple platforms (NetEase, QQ, Kugou) and local files, with lyrics retrieval and playback control.MIT