mcp-lyricscom 2.0.1 → 2.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/CHANGELOG.md +9 -0
- package/README.md +5 -4
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/lyricscom/client.d.ts +2 -1
- package/dist/lyricscom/client.js +1 -1
- package/dist/lyricscom/client.js.map +1 -1
- package/package.json +13 -12
- package/server.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.0
|
|
4
|
+
|
|
5
|
+
- **Node.js 24.22.1 or later**, inside the Node.js 24 line. The package, the
|
|
6
|
+
extension manifest and the image all declare it.
|
|
7
|
+
- **zod `^4.6.7`.** Below 4.6.6, zod can exhaust memory while
|
|
8
|
+
validating oversized constrained collections, and the range accepts none of
|
|
9
|
+
those versions.
|
|
10
|
+
- **Dependency floors:** `@modelcontextprotocol/sdk` `^1.32.2`.
|
|
11
|
+
|
|
3
12
|
## 2.0.1
|
|
4
13
|
|
|
5
14
|
- **Every tool is documented, with its arguments and what its answer carries.**
|
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
[](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.smeet666/mcp-lyricscom)
|
|
7
7
|
[](https://glama.ai/mcp/servers/smeet666/mcp-lyricscom)
|
|
8
8
|
[](https://m8ven.ai/mcp/smeet666-mcp-lyricscom-pptp4t)
|
|
9
|
+
[](https://lobehub.com/mcp/smeet666-mcp-lyricscom)
|
|
9
10
|
[](https://cursor.com/en/install-mcp?name=lyricscom&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1seXJpY3Njb20iXX0%3D)
|
|
10
11
|
[](https://insiders.vscode.dev/redirect/mcp/install?name=lyricscom&config=%7B%22name%22%3A%22lyricscom%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22mcp-lyricscom%22%5D%7D)
|
|
11
12
|
|
|
@@ -59,7 +60,7 @@ Node 24 or later is required, and no environment variable has to be set.
|
|
|
59
60
|
"mcpServers": {
|
|
60
61
|
"lyricscom": {
|
|
61
62
|
"command": "docker",
|
|
62
|
-
"args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-lyricscom:2.0
|
|
63
|
+
"args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-lyricscom:2.1.0"]
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
}
|
|
@@ -71,7 +72,7 @@ because a TTY rewrites the stream. The container needs outbound HTTPS to
|
|
|
71
72
|
|
|
72
73
|
### Bundle, without npm
|
|
73
74
|
|
|
74
|
-
Download `mcp-lyricscom-2.0.
|
|
75
|
+
Download `mcp-lyricscom-2.1.0.mcpb` from
|
|
75
76
|
[the latest release](https://github.com/smeet666/mcp-lyricscom/releases/latest)
|
|
76
77
|
and open it. A client that supports MCP bundles installs it on its own, with no
|
|
77
78
|
npm and no configuration file to edit. The bundle carries its dependencies, so
|
|
@@ -315,7 +316,7 @@ renseigner.
|
|
|
315
316
|
"mcpServers": {
|
|
316
317
|
"lyricscom": {
|
|
317
318
|
"command": "docker",
|
|
318
|
-
"args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-lyricscom:2.0
|
|
319
|
+
"args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-lyricscom:2.1.0"]
|
|
319
320
|
}
|
|
320
321
|
}
|
|
321
322
|
}
|
|
@@ -328,7 +329,7 @@ aucun identifiant.
|
|
|
328
329
|
|
|
329
330
|
### Bundle, sans npm
|
|
330
331
|
|
|
331
|
-
Téléchargez `mcp-lyricscom-2.0.
|
|
332
|
+
Téléchargez `mcp-lyricscom-2.1.0.mcpb` depuis
|
|
332
333
|
[la dernière publication](https://github.com/smeet666/mcp-lyricscom/releases/latest)
|
|
333
334
|
et ouvrez-le. Un client qui gère les bundles MCP l'installe seul, sans npm et
|
|
334
335
|
sans fichier de configuration à modifier. Le bundle emporte ses dépendances, donc
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
8
8
|
import process from "process";
|
|
9
9
|
|
|
10
10
|
// src/version.ts
|
|
11
|
-
var PKG_VERSION = "2.0
|
|
11
|
+
var PKG_VERSION = "2.1.0";
|
|
12
12
|
var REPO_URL = "https://github.com/smeet666/mcp-lyricscom";
|
|
13
13
|
|
|
14
14
|
// src/config.ts
|
|
@@ -969,7 +969,7 @@ function renderResultList(results) {
|
|
|
969
969
|
return results.map((result, index) => {
|
|
970
970
|
const year = result.year ? ` (${result.year})` : "";
|
|
971
971
|
const album = result.album ? ` [${result.album}]` : "";
|
|
972
|
-
const head = `${index + 1}. ${result.title} \
|
|
972
|
+
const head = `${index + 1}. ${result.title} \xB7 ${result.artist}${year}${album} \xB7 id: ${result.id}`;
|
|
973
973
|
return result.excerpt ? `${head}
|
|
974
974
|
"${result.excerpt}"` : head;
|
|
975
975
|
}).join("\n");
|
|
@@ -1062,8 +1062,8 @@ async function runGetLyrics(client, args) {
|
|
|
1062
1062
|
if (cached) {
|
|
1063
1063
|
notes.push("Served from this server's short-lived in-memory cache.");
|
|
1064
1064
|
}
|
|
1065
|
-
const credit = [data.title, data.artist].filter(Boolean).join(" \
|
|
1066
|
-
const attribution = `${credit || "Lyrics"} via lyrics.com
|
|
1065
|
+
const credit = [data.title, data.artist].filter(Boolean).join(" \xB7 ");
|
|
1066
|
+
const attribution = `${credit || "Lyrics"} via lyrics.com (${data.url})`;
|
|
1067
1067
|
if (!data.hasLyrics) {
|
|
1068
1068
|
const structured2 = {
|
|
1069
1069
|
status: "no_lyrics",
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/version.ts","../src/server.ts","../src/lyricscom/cache.ts","../src/errors.ts","../src/lyricscom/parsers/detectThrottle.ts","../src/lyricscom/rateLimiter.ts","../src/lyricscom/http.ts","../src/lyricscom/parsers/searchResults.ts","../src/text/normalize.ts","../src/lyricscom/urls.ts","../src/lyricscom/parsers/selectors.ts","../src/lyricscom/parsers/songPage.ts","../src/lyricscom/client.ts","../src/tools/getLyrics.ts","../src/text/keywordIndex.ts","../src/text/excerpt.ts","../src/tools/arguments.ts","../src/tools/shared.ts","../src/tools/searchLyrics.ts","../src/text/dedupe.ts","../src/tools/searchSongs.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Entry point: an MCP server for lyrics.com over stdio.\n */\n\nimport process from \"node:process\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { createServer } from \"./server.js\";\n\nasync function main(): Promise<void> {\n const config = loadConfig();\n const logger = createLogger(config.logLevel);\n const server = createServer({ config, logger });\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n const shutdown = () => {\n // The close is attempted, and a close that fails is no reason to stay\n // alive: the process leaves either way.\n server\n .close()\n .catch(() => undefined)\n .finally(() => process.exit(0));\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((error: unknown) => {\n process.stderr.write(\n `[mcp-lyricscom] fatal: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`,\n );\n process.exit(1);\n});\n","/**\n * Runtime configuration, read from environment variables.\n *\n * A bad value never crashes the process: an MCP server that dies at startup\n * because of a typo in a client config file is very hard to diagnose from the\n * host application, so invalid input is clamped and reported on stderr.\n */\n\nimport process from \"node:process\";\nimport { PKG_VERSION, REPO_URL } from \"./version.js\";\n\nexport type LogLevel = \"silent\" | \"error\" | \"info\" | \"debug\";\n\nexport interface Config {\n userAgent: string;\n minIntervalMs: number;\n timeoutMs: number;\n maxRetries: number;\n cacheTtlMs: number;\n cacheMaxEntries: number;\n logLevel: LogLevel;\n}\n\n/**\n * Identifies the client honestly. lyrics.com serves this fine; it blocks\n * generic tool agents such as `curl/x.y.z`, which is what the override is for.\n */\nexport const DEFAULT_USER_AGENT = `mcp-lyricscom/${PKG_VERSION} (+${REPO_URL})`;\n\nexport const DEFAULTS = {\n minIntervalMs: 1100,\n timeoutMs: 15_000,\n maxRetries: 3,\n cacheTtlMs: 15 * 60 * 1000,\n cacheMaxEntries: 200,\n logLevel: \"error\" as LogLevel,\n};\n\n/**\n * Floor on the request interval, enforced regardless of configuration.\n *\n * The pacing is what keeps this client from behaving like a crawler against a\n * site that has no API and no published rate limit. Leaving it configurable down\n * to zero would make the politeness of every installation depend on whoever\n * edited a JSON file, so a value below this floor is refused and the default is\n * used instead.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 500;\n\nconst LOG_LEVELS: LogLevel[] = [\"silent\", \"error\", \"info\", \"debug\"];\n\ninterface NumericRange {\n min: number;\n max: number;\n fallback: number;\n}\n\nfunction readNumber(name: string, env: NodeJS.ProcessEnv, range: NumericRange): number {\n const raw = env[name];\n if (raw === undefined || raw.trim() === \"\") {\n return range.fallback;\n }\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`${name}=\"${raw}\" is not a number, using ${range.fallback}`);\n return range.fallback;\n }\n const clamped = Math.min(range.max, Math.max(range.min, Math.round(parsed)));\n if (clamped !== Math.round(parsed)) {\n warn(`${name}=${raw} is out of range, clamped to ${clamped}`);\n }\n return clamped;\n}\n\nfunction warn(message: string): void {\n process.stderr.write(`[mcp-lyricscom] ${message}\\n`);\n}\n\n/**\n * Read the request interval, refusing anything below the floor.\n *\n * A value under the floor falls back to the default rather than to the floor\n * itself: someone who set 0 was not asking for 500, they were asking for no\n * pacing at all, and the safe reading of that request is to ignore it.\n */\nfunction readInterval(env: NodeJS.ProcessEnv): number {\n const raw = env.LYRICSCOM_MIN_INTERVAL_MS;\n if (raw === undefined || raw.trim() === \"\") {\n return DEFAULTS.minIntervalMs;\n }\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`LYRICSCOM_MIN_INTERVAL_MS=\"${raw}\" is not a number, using ${DEFAULTS.minIntervalMs}ms`);\n return DEFAULTS.minIntervalMs;\n }\n\n const rounded = Math.round(parsed);\n if (rounded < MIN_ALLOWED_INTERVAL_MS) {\n warn(\n `LYRICSCOM_MIN_INTERVAL_MS=${raw} is below the ${MIN_ALLOWED_INTERVAL_MS}ms floor and was ignored; ` +\n `using ${DEFAULTS.minIntervalMs}ms. This floor keeps the client from hammering lyrics.com.`,\n );\n return DEFAULTS.minIntervalMs;\n }\n\n return Math.min(60_000, rounded);\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const rawUserAgent = env.LYRICSCOM_USER_AGENT?.trim();\n const rawLogLevel = env.LYRICSCOM_LOG_LEVEL?.trim().toLowerCase();\n\n let logLevel = DEFAULTS.logLevel;\n if (rawLogLevel) {\n if (LOG_LEVELS.includes(rawLogLevel as LogLevel)) {\n logLevel = rawLogLevel as LogLevel;\n } else {\n warn(`LYRICSCOM_LOG_LEVEL=\"${rawLogLevel}\" is unknown, using \"${DEFAULTS.logLevel}\"`);\n }\n }\n\n return {\n userAgent: rawUserAgent || DEFAULT_USER_AGENT,\n minIntervalMs: readInterval(env),\n timeoutMs: readNumber(\"LYRICSCOM_TIMEOUT_MS\", env, {\n min: 1000,\n max: 120_000,\n fallback: DEFAULTS.timeoutMs,\n }),\n maxRetries: readNumber(\"LYRICSCOM_MAX_RETRIES\", env, {\n min: 0,\n max: 10,\n fallback: DEFAULTS.maxRetries,\n }),\n cacheTtlMs: readNumber(\"LYRICSCOM_CACHE_TTL_MS\", env, {\n min: 0,\n max: 24 * 60 * 60 * 1000,\n fallback: DEFAULTS.cacheTtlMs,\n }),\n cacheMaxEntries: readNumber(\"LYRICSCOM_CACHE_MAX_ENTRIES\", env, {\n min: 0,\n max: 10_000,\n fallback: DEFAULTS.cacheMaxEntries,\n }),\n logLevel,\n };\n}\n\nconst LEVEL_RANK: Record<LogLevel, number> = { silent: 0, error: 1, info: 2, debug: 3 };\n\n/**\n * Logs go to stderr without exception. On a stdio transport, stdout carries the\n * protocol and any stray write there corrupts the session.\n */\nexport function createLogger(level: LogLevel) {\n const emit = (at: LogLevel, message: string) => {\n if (LEVEL_RANK[level] >= LEVEL_RANK[at]) {\n process.stderr.write(`[mcp-lyricscom] ${message}\\n`);\n }\n };\n return {\n error: (message: string) => emit(\"error\", message),\n info: (message: string) => emit(\"info\", message),\n debug: (message: string) => emit(\"debug\", message),\n };\n}\n\nexport type Logger = ReturnType<typeof createLogger>;\n","/**\n * Kept in sync with package.json. The User-Agent embeds it, so it must not\n * drift; the release checklist bumps both together.\n */\nexport const PKG_VERSION = \"2.0.1\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-lyricscom\";\n","/**\n * MCP server wiring.\n *\n * One client, one rate limiter and one cache are shared by all three tools:\n * per-tool instances would let three tools each open their own request stream\n * and defeat the throttling protection.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { Config, Logger } from \"./config.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { LyricsComClient } from \"./lyricscom/client.js\";\nimport {\n getLyricsDescription,\n getLyricsInput,\n getLyricsOutputShape,\n runGetLyrics,\n} from \"./tools/getLyrics.js\";\nimport type { GetLyricsArgs } from \"./tools/getLyrics.js\";\nimport {\n runSearchLyrics,\n searchLyricsDescription,\n searchLyricsInput,\n searchLyricsOutputShape,\n} from \"./tools/searchLyrics.js\";\nimport type { SearchLyricsArgs } from \"./tools/searchLyrics.js\";\nimport {\n runSearchSongs,\n searchSongsDescription,\n searchSongsInput,\n searchSongsOutputShape,\n} from \"./tools/searchSongs.js\";\nimport type { SearchSongsArgs } from \"./tools/searchSongs.js\";\nimport { PKG_VERSION } from \"./version.js\";\n\nexport interface CreateServerOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\nconst READ_ONLY = {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n} as const;\n\nexport function createServer(options: CreateServerOptions = {}): McpServer {\n const config = options.config ?? loadConfig();\n const logger = options.logger ?? createLogger(config.logLevel);\n const client = new LyricsComClient({\n config,\n logger,\n ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),\n });\n\n const server = new McpServer(\n { name: \"mcp-lyricscom\", version: PKG_VERSION },\n {\n instructions:\n \"Tools for looking up song lyrics on lyrics.com. No API key is needed. \" +\n \"Typical flow: search_lyrics (by a word in the lyrics) or search_songs (by title) to get a song id, \" +\n \"then get_lyrics with that id. When you show lyrics to a user, credit the artist and link the source URL. \" +\n \"A 'throttled' error means lyrics.com is rate limiting this client, not that the song does not exist.\",\n },\n );\n\n server.registerTool(\n \"search_lyrics\",\n {\n title: \"Search lyrics by word\",\n description: searchLyricsDescription,\n inputSchema: searchLyricsInput,\n outputSchema: z.object(searchLyricsOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runSearchLyrics(client, args as SearchLyricsArgs),\n );\n\n server.registerTool(\n \"search_songs\",\n {\n title: \"Search songs by title\",\n description: searchSongsDescription,\n inputSchema: searchSongsInput,\n outputSchema: z.object(searchSongsOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runSearchSongs(client, args as SearchSongsArgs),\n );\n\n server.registerTool(\n \"get_lyrics\",\n {\n title: \"Get full lyrics\",\n description: getLyricsDescription,\n inputSchema: getLyricsInput,\n outputSchema: z.object(getLyricsOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runGetLyrics(client, args as GetLyricsArgs),\n );\n\n logger.info(\n `ready: user-agent=\"${config.userAgent}\", min interval ${config.minIntervalMs}ms, cache ${config.cacheTtlMs}ms`,\n );\n\n return server;\n}\n","/**\n * In-memory TTL + LRU cache.\n *\n * Nothing is written to disk. The cache exists so that repeated questions about\n * the same song inside one conversation do not hit lyrics.com again, not to\n * build a local copy of the site.\n *\n * A Map iterates in insertion order, so re-inserting on every hit is enough to\n * make the first key the least recently used one.\n */\n\ninterface Entry<V> {\n value: V;\n expiresAt: number;\n}\n\nexport class TtlLruCache<V> {\n private readonly store = new Map<string, Entry<V>>();\n\n constructor(\n private readonly maxEntries: number,\n private readonly ttlMs: number,\n ) {}\n\n get(key: string): V | undefined {\n const entry = this.store.get(key);\n if (!entry) {\n return undefined;\n }\n if (Date.now() >= entry.expiresAt) {\n this.store.delete(key);\n return undefined;\n }\n this.store.delete(key);\n this.store.set(key, entry);\n return entry.value;\n }\n\n set(key: string, value: V): void {\n if (this.maxEntries <= 0 || this.ttlMs <= 0) {\n return;\n }\n this.store.delete(key);\n this.store.set(key, { value, expiresAt: Date.now() + this.ttlMs });\n while (this.store.size > this.maxEntries) {\n const oldest = this.store.keys().next();\n if (oldest.done) {\n break;\n }\n this.store.delete(oldest.value);\n }\n }\n\n get size(): number {\n return this.store.size;\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","/**\n * Error taxonomy surfaced to the calling model.\n *\n * The point of this file is that an LLM must be able to tell \"lyrics.com pushed\n * back on me\" apart from \"there is no such song\". The reference implementation\n * this server replaces collapsed every failure into an empty list, which made\n * rate limiting look like an absence of results.\n */\n\nexport type ErrorCode =\n | \"throttled\"\n | \"blocked_user_agent\"\n | \"not_found\"\n | \"invalid_input\"\n | \"parse_failure\"\n | \"network_error\"\n | \"timeout\";\n\nexport interface ErrorDetails {\n url?: string;\n status?: number;\n retryAfterMs?: number;\n hint?: string;\n}\n\nexport class LyricsComError extends Error {\n constructor(\n readonly code: ErrorCode,\n message: string,\n readonly details: ErrorDetails = {},\n ) {\n super(message);\n this.name = \"LyricsComError\";\n }\n}\n\nconst ISSUES_URL = \"https://github.com/smeet666/mcp-lyricscom/issues\";\n\nexport function throttled(url: string, retryAfterMs: number): LyricsComError {\n return new LyricsComError(\n \"throttled\",\n \"lyrics.com is rate limiting this client. It answered with an empty response instead of a page. \" +\n \"This does NOT mean there are no results for this query.\",\n {\n url,\n retryAfterMs,\n hint:\n \"Wait at least a minute before calling the same tool again with the same arguments. Once lyrics.com \" +\n \"starts throttling, the window commonly lasts several minutes, so retrying immediately will fail again. \" +\n \"If it keeps happening, raise LYRICSCOM_MIN_INTERVAL_MS in your MCP client configuration.\",\n },\n );\n}\n\nexport function blockedUserAgent(url: string): LyricsComError {\n return new LyricsComError(\n \"blocked_user_agent\",\n \"lyrics.com refused the request because of the User-Agent this server sends (HTTP 403).\",\n {\n url,\n status: 403,\n hint:\n \"Set the LYRICSCOM_USER_AGENT environment variable in your MCP client configuration to a different \" +\n \"value. See the Configuration section of the project README.\",\n },\n );\n}\n\nexport function notFound(url: string): LyricsComError {\n return new LyricsComError(\"not_found\", \"lyrics.com has no page at this address.\", {\n url,\n status: 404,\n hint: \"Check the song id. Ids come from the search tools; they are not stable to guess by hand.\",\n });\n}\n\nexport function parseFailure(url: string, what: string): LyricsComError {\n return new LyricsComError(\n \"parse_failure\",\n `The page loaded but the expected lyrics.com markup was not found (${what}). The site layout may have changed.`,\n { url, hint: `Please report this, with the query you used, at ${ISSUES_URL}` },\n );\n}\n\nexport function invalidInput(message: string, hint?: string): LyricsComError {\n return new LyricsComError(\"invalid_input\", message, hint ? { hint } : {});\n}\n","/**\n * Response classification.\n *\n * lyrics.com does not answer rate limiting with 429. It answers with HTTP 202\n * and a zero-byte body, which a naive client reads as a successful but empty\n * page. Every response therefore goes through this classifier before any\n * parsing happens.\n */\n\nconst CLOSING_HTML_TAG = /<\\/html>/i;\n\nexport type ResponseVerdict =\n | { kind: \"ok\"; html: string }\n | { kind: \"throttled\"; reason: ThrottleReason }\n | { kind: \"blocked\"; status: number }\n | { kind: \"not-found\" }\n | { kind: \"server-error\"; status: number };\n\nexport type ThrottleReason =\n | \"status-202\"\n | \"status-429\"\n | \"status-503\"\n | \"empty-body\"\n | \"short-body\";\n\n/**\n * A real search page weighs about 187 KB and a song page 60 KB or more, so a\n * body this small is never a genuine lyrics.com page.\n */\nexport const MIN_PLAUSIBLE_HTML = 2000;\n\nexport function classifyResponse(status: number, body: string): ResponseVerdict {\n if (status === 202) {\n return { kind: \"throttled\", reason: \"status-202\" };\n }\n if (status === 429) {\n return { kind: \"throttled\", reason: \"status-429\" };\n }\n if (status === 503) {\n return { kind: \"throttled\", reason: \"status-503\" };\n }\n if (status === 403) {\n return { kind: \"blocked\", status };\n }\n if (status === 404) {\n return { kind: \"not-found\" };\n }\n if (status >= 500) {\n return { kind: \"server-error\", status };\n }\n\n const trimmed = body.trim();\n if (trimmed.length === 0) {\n return { kind: \"throttled\", reason: \"empty-body\" };\n }\n if (trimmed.length < MIN_PLAUSIBLE_HTML && !CLOSING_HTML_TAG.test(trimmed)) {\n return { kind: \"throttled\", reason: \"short-body\" };\n }\n\n return { kind: \"ok\", html: body };\n}\n","/**\n * Serial request queue with an adaptive minimum interval.\n *\n * lyrics.com tolerates a steady trickle but stops answering after a short burst,\n * so requests are run one at a time with a floor on the gap between starts.\n * When the site does push back, the interval doubles and then decays back down\n * as requests succeed, which recovers faster than a fixed delay and behaves\n * better than hammering at a constant rate.\n */\n\nexport interface RateLimiterOptions {\n minIntervalMs: number;\n maxIntervalMs?: number;\n}\n\nconst DEFAULT_MAX_INTERVAL_MS = 10_000;\n\nexport class RateLimiter {\n private readonly baseIntervalMs: number;\n private readonly maxIntervalMs: number;\n private intervalMs: number;\n /** Tail of the queue: each task chains onto the previous one. */\n private tail: Promise<unknown> = Promise.resolve();\n private lastStart = 0;\n\n constructor(options: RateLimiterOptions) {\n this.baseIntervalMs = Math.max(0, options.minIntervalMs);\n this.maxIntervalMs = Math.max(\n this.baseIntervalMs,\n options.maxIntervalMs ?? DEFAULT_MAX_INTERVAL_MS,\n );\n this.intervalMs = this.baseIntervalMs;\n }\n\n get currentIntervalMs(): number {\n return this.intervalMs;\n }\n\n /** Queue a task. Tasks run in call order, one at a time. */\n schedule<T>(task: () => Promise<T>): Promise<T> {\n const run = this.tail.then(async () => task());\n // The queue must keep draining even when a task rejects.\n this.tail = run.then(\n () => undefined,\n () => undefined,\n );\n return run;\n }\n\n /**\n * Wait for this request's slot, then claim it.\n *\n * Called once per upstream request rather than once per task, because a task\n * runs a whole retry chain: stamping only its start would let the next task\n * follow the chain's last request with no gap at all.\n */\n async beforeRequest(): Promise<void> {\n await this.waitForSlot();\n this.lastStart = Date.now();\n }\n\n /** Called after the site signals it is under load. */\n penalize(): void {\n const next = this.intervalMs === 0 ? 250 : this.intervalMs * 2;\n this.intervalMs = Math.min(this.maxIntervalMs, next);\n }\n\n /** Called after a success, so a single hiccup does not slow things down forever. */\n relax(): void {\n this.intervalMs = Math.max(this.baseIntervalMs, Math.floor(this.intervalMs * 0.75));\n }\n\n private async waitForSlot(): Promise<void> {\n if (this.intervalMs === 0 || this.lastStart === 0) {\n return;\n }\n const elapsed = Date.now() - this.lastStart;\n // Clamped to the interval: a clock stepped backwards, by NTP or a resumed\n // virtual machine, would otherwise make this wait for the size of the step,\n // and the queue is serial so every pending request would wait behind it.\n const remaining = Math.min(this.intervalMs, this.intervalMs - elapsed);\n if (remaining > 0) {\n await sleep(remaining);\n }\n }\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * HTTP layer: one GET, classified, with backoff.\n *\n * The retry policy is domain specific rather than generic, because the failure\n * this server exists to handle does not look like a failure at the transport\n * level: lyrics.com answers a throttled request with HTTP 202 and an empty\n * body. Only `classifyResponse` can tell that apart from a real page.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport { blockedUserAgent, LyricsComError, notFound, throttled } from \"../errors.js\";\nimport { classifyResponse } from \"./parsers/detectThrottle.js\";\nimport { type RateLimiter, sleep } from \"./rateLimiter.js\";\n\nconst BACKOFF_BASE_MS = 2000;\nconst BACKOFF_FACTOR = 2;\nconst BACKOFF_MAX_MS = 20_000;\n\n/** Exponential backoff with jitter, so parallel clients do not resynchronise. */\nexport function backoffDelay(attempt: number, random: () => number = Math.random): number {\n const uncapped = BACKOFF_BASE_MS * BACKOFF_FACTOR ** attempt;\n const capped = Math.min(BACKOFF_MAX_MS, uncapped);\n return Math.round(capped * (0.5 + random() * 0.5));\n}\n\nexport interface HttpDeps {\n config: Config;\n limiter: RateLimiter;\n logger: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/**\n * Fetch one page as HTML, retrying transient conditions.\n *\n * The whole retry loop, backoff sleeps included, runs inside a single limiter\n * slot. Waiting outside it would let a queued request slip into the same\n * throttling window that the current one is backing away from.\n */\nexport async function fetchHtml(url: string, deps: HttpDeps): Promise<string> {\n const { config, limiter, logger } = deps;\n const doFetch = deps.fetchImpl ?? fetch;\n\n return await limiter.schedule(async () => {\n let lastError: LyricsComError | undefined;\n\n // Set when the site says how long to stay away; it replaces our own guess\n // for the next attempt. Applied here rather than where it is read, so no\n // wait is ever served after the last attempt, when nobody would use it.\n let askedWaitMs: number | null = null;\n\n for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {\n if (attempt > 0) {\n const delay = Math.min(askedWaitMs ?? backoffDelay(attempt - 1), BACKOFF_MAX_MS);\n askedWaitMs = null;\n logger.info(`retry ${attempt}/${config.maxRetries} in ${delay}ms for ${url}`);\n await sleep(delay);\n }\n\n let status: number;\n let body: string;\n let retryAfterMs: number | null = null;\n try {\n await limiter.beforeRequest();\n const response = await doFetch(url, {\n headers: {\n \"User-Agent\": config.userAgent,\n Accept: \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n \"Accept-Language\": \"en-US,en;q=0.9,fr;q=0.8\",\n },\n redirect: \"follow\",\n signal: AbortSignal.timeout(config.timeoutMs),\n });\n status = response.status;\n retryAfterMs = parseRetryAfter(response.headers.get(\"retry-after\"));\n body = await response.text();\n } catch (error) {\n lastError = asTransportError(error, url);\n logger.debug(`${lastError.code} for ${url}: ${lastError.message}`);\n continue;\n }\n\n const verdict = classifyResponse(status, body);\n switch (verdict.kind) {\n case \"ok\":\n limiter.relax();\n return verdict.html;\n\n case \"throttled\":\n limiter.penalize();\n logger.info(\n `throttled (${verdict.reason}) on ${url}, interval now ${limiter.currentIntervalMs}ms`,\n );\n // A server that says when to come back knows better than our own guess.\n askedWaitMs = retryAfterMs;\n lastError = throttled(url, retryAfterMs ?? backoffDelay(attempt));\n continue;\n\n case \"server-error\":\n lastError = new LyricsComError(\n \"network_error\",\n `lyrics.com returned HTTP ${verdict.status}.`,\n {\n url,\n status: verdict.status,\n },\n );\n continue;\n\n // Retrying these would only repeat the same answer.\n case \"blocked\":\n throw blockedUserAgent(url);\n case \"not-found\":\n throw notFound(url);\n\n // Every kind the classifier declares is handled above, and the linter\n // holds that. This clause is the runtime backstop: a response nothing\n // above matched leaves as an error rather than as a silent retry.\n default:\n throw new LyricsComError(\n \"network_error\",\n `lyrics.com returned a response this client cannot classify for ${url}.`,\n { url },\n );\n }\n }\n\n throw lastError ?? new LyricsComError(\"network_error\", `Could not fetch ${url}.`, { url });\n });\n}\n\n/** `Retry-After` carries either seconds or an HTTP date. */\nfunction parseRetryAfter(raw: string | null): number | null {\n if (!raw) {\n return null;\n }\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) {\n return Math.round(seconds * 1000);\n }\n const when = Date.parse(raw);\n if (Number.isNaN(when)) {\n return null;\n }\n return Math.max(0, when - Date.now());\n}\n\nfunction asTransportError(error: unknown, url: string): LyricsComError {\n if (error instanceof LyricsComError) {\n return error;\n }\n const name = error instanceof Error ? error.name : \"\";\n if (name === \"TimeoutError\" || name === \"AbortError\") {\n return new LyricsComError(\"timeout\", \"lyrics.com did not answer in time.\", {\n url,\n hint: \"Raise LYRICSCOM_TIMEOUT_MS if this happens often on a slow connection.\",\n });\n }\n const message = error instanceof Error ? error.message : String(error);\n return new LyricsComError(\"network_error\", `Could not reach lyrics.com: ${message}`, { url });\n}\n","/**\n * Parser for a lyrics.com search results page.\n *\n * Page 1 (`/lyrics/{term}`) and later pages (`/serp.php?st=...&p=N`) share the\n * same markup, so one parser covers both.\n */\n\nimport process from \"node:process\";\nimport { load } from \"cheerio\";\nimport { parseFailure } from \"../../errors.js\";\nimport type { SearchPage, SongResult } from \"../../types.js\";\nimport { cleanInlineText, cleanLyricsText } from \"../../text/normalize.js\";\nimport { extractSongId, RESULTS_PER_PAGE, toAbsoluteUrl } from \"../urls.js\";\nimport { SEL } from \"./selectors.js\";\n\n/** A year a record can plausibly carry, from the 1800s to this century. */\nconst YEAR = /\\b(1[89]\\d{2}|20\\d{2})\\b/;\n\nexport interface ParseSearchOptions {\n page: number;\n /** Reported in errors so a failure points at the request that caused it. */\n url: string;\n}\n\nexport function parseSearchResults(html: string, options: ParseSearchOptions): SearchPage {\n const $ = load(html);\n const rows = $(SEL.serpItem);\n\n if (rows.length === 0) {\n // An empty result set still renders the page chrome. Its absence means the\n // response was not the page we asked for, which must not be reported as\n // \"no songs found\".\n if ($(SEL.serpShell).length === 0) {\n throw parseFailure(options.url, \"no result rows and no recognizable search page structure\");\n }\n return { results: [], page: options.page, rawCount: 0, hasMore: false };\n }\n\n const results: SongResult[] = [];\n let skipped = 0;\n\n rows.each((_, element) => {\n const row = $(element);\n const titleLink = row.find(SEL.resultTitle).first();\n const href = titleLink.attr(\"href\");\n const title = cleanInlineText(titleLink.text());\n const artist = cleanInlineText(row.find(SEL.resultArtist).first().text());\n\n const id = href ? extractSongId(href) : null;\n if (!(id && title && artist)) {\n skipped += 1;\n return;\n }\n\n const album = cleanInlineText(row.find(SEL.resultAlbum).first().text()) || null;\n const year = parseYear(row.find(SEL.resultYear).first().text());\n const snippetRaw = row.find(SEL.resultSnippet).first().text();\n const snippet = snippetRaw ? cleanLyricsText(snippetRaw) || null : null;\n\n results.push({\n id,\n url: toAbsoluteUrl(href as string),\n title,\n artist,\n album,\n year,\n snippet,\n });\n });\n\n if (results.length === 0) {\n throw parseFailure(options.url, `${rows.length} result rows matched but none could be read`);\n }\n if (skipped > 0) {\n process.stderr.write(\n `[mcp-lyricscom] skipped ${skipped} unreadable result rows on ${options.url}\\n`,\n );\n }\n\n return {\n results,\n page: options.page,\n rawCount: rows.length,\n hasMore: rows.length >= RESULTS_PER_PAGE,\n };\n}\n\n/** Year cells look like \"1971\" but are sometimes decorated or empty. */\nfunction parseYear(raw: string): number | null {\n const match = YEAR.exec(raw);\n if (!match?.[1]) {\n return null;\n }\n return Number.parseInt(match[1], 10);\n}\n","/**\n * Text cleaning for scraped content: HTML entities, mojibake, whitespace.\n */\n\n/** The letters UTF-8 read as Latin-1 leaves behind. */\nconst MOJIBAKE_MARKS = /[ÃÂ]/;\n\nconst HTML_ENTITIES: Record<string, string> = {\n amp: \"&\",\n lt: \"<\",\n gt: \">\",\n quot: '\"',\n apos: \"'\",\n nbsp: \" \",\n hellip: \"…\",\n mdash: \"—\",\n ndash: \"–\",\n rsquo: \"’\",\n lsquo: \"‘\",\n ldquo: \"“\",\n rdquo: \"”\",\n};\n\n/** Decode named and numeric HTML entities. */\nexport function unescapeHtml(text: string): string {\n if (!text) {\n return text;\n }\n return text.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (match, entity: string) => {\n if (entity.startsWith(\"#\")) {\n const isHex = entity[1] === \"x\" || entity[1] === \"X\";\n const codePoint = Number.parseInt(isHex ? entity.slice(2) : entity.slice(1), isHex ? 16 : 10);\n if (Number.isNaN(codePoint) || codePoint < 0 || codePoint > 0x10ffff) {\n return match;\n }\n try {\n return String.fromCodePoint(codePoint);\n } catch {\n return match;\n }\n }\n return HTML_ENTITIES[entity.toLowerCase()] ?? match;\n });\n}\n\n/**\n * Repair UTF-8 bytes that were decoded as Latin-1, which turns \"é\" into \"é\".\n * lyrics.com serves French titles that occasionally arrive in this state.\n * Re-encoding through latin1 recovers the original text; the round trip is only\n * kept when it removes replacement characters, so correct text is never touched.\n */\nexport function fixEncodingIssues(text: string): string {\n if (!(text && MOJIBAKE_MARKS.test(text))) {\n return text;\n }\n try {\n const repaired = Buffer.from(text, \"latin1\").toString(\"utf8\");\n if (repaired.includes(\"�\")) {\n return text;\n }\n return repaired;\n } catch {\n return text;\n }\n}\n\n/** Collapse runs of whitespace and trim. */\nexport function collapseWhitespace(text: string): string {\n return text.replace(/\\s+/g, \" \").trim();\n}\n\n/** Full cleaning pass for a scraped title or artist name. */\nexport function cleanInlineText(text: string): string {\n if (!text) {\n return \"\";\n }\n return collapseWhitespace(fixEncodingIssues(unescapeHtml(text)));\n}\n\n/**\n * Normalize a lyrics block: trim each line, drop leading and trailing blank\n * lines, and collapse runs of blank lines to a single verse separator.\n */\nexport function cleanLyricsText(text: string): string {\n if (!text) {\n return \"\";\n }\n const normalized = fixEncodingIssues(unescapeHtml(text)).replace(/\\r\\n?/g, \"\\n\");\n const lines = normalized.split(\"\\n\").map((line) => line.trim());\n\n const out: string[] = [];\n for (const line of lines) {\n if (line === \"\" && (out.length === 0 || out.at(-1) === \"\")) {\n continue;\n }\n out.push(line);\n }\n while (out.length > 0 && out.at(-1) === \"\") {\n out.pop();\n }\n return out.join(\"\\n\");\n}\n","/**\n * URL construction and validation for lyrics.com.\n */\n\nimport { invalidInput } from \"../errors.js\";\n\nconst ABSOLUTE_URL = /^https?:\\/\\//i;\nconst DIGITS_ONLY = /^\\d+$/;\n\nexport const BASE_URL = \"https://www.lyrics.com\";\n\n/** lyrics.com renders 24 rows per search page. */\nexport const RESULTS_PER_PAGE = 24;\n\n/**\n * Song page paths come in two shapes: the current `/lyric-lf/{id}/...` and the\n * older `/lyric/{id}/...` still present in archived links.\n */\nexport const SONG_HREF_RE = /^\\/lyric(?:-lf)?\\/(\\d+)\\//;\n\nconst ALLOWED_HOSTS = new Set([\"lyrics.com\", \"www.lyrics.com\"]);\n\n/**\n * Percent-encode a search term the way lyrics.com expects, with spaces as `+`.\n */\nfunction encodeTerm(term: string): string {\n return encodeURIComponent(term).replace(/%20/g, \"+\");\n}\n\n/**\n * Page 1 lives at a path, later pages at a query string. Both return the same\n * markup, so the parser does not need to know which one produced it.\n */\nexport function buildSearchUrl(term: string, page: number): string {\n const encoded = encodeTerm(term);\n if (page <= 1) {\n return `${BASE_URL}/lyrics/${encoded}`;\n }\n return `${BASE_URL}/serp.php?st=${encoded}&p=${page}`;\n}\n\nexport function buildSongUrl(id: string): string {\n return `${BASE_URL}/lyric-lf/${id}/`;\n}\n\n/** True only for lyrics.com itself, so a hostile URL cannot be used as a proxy. */\nexport function isLyricsComHost(rawUrl: string): boolean {\n let parsed: URL;\n try {\n parsed = new URL(rawUrl);\n } catch {\n return false;\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n return false;\n }\n return ALLOWED_HOSTS.has(parsed.hostname.toLowerCase());\n}\n\n/** Extract the numeric song id from a lyrics.com song path or absolute URL. */\nexport function extractSongId(hrefOrUrl: string): string | null {\n let path = hrefOrUrl;\n if (ABSOLUTE_URL.test(hrefOrUrl)) {\n if (!isLyricsComHost(hrefOrUrl)) {\n return null;\n }\n path = new URL(hrefOrUrl).pathname;\n }\n if (!path.startsWith(\"/\")) {\n path = `/${path}`;\n }\n const match = SONG_HREF_RE.exec(path);\n return match?.[1] ?? null;\n}\n\n/**\n * Resolve the `id` / `url` pair accepted by get_lyrics into a canonical\n * request URL. `id` wins when both are given.\n */\nexport function resolveSongRef(input: { id?: string; url?: string }): { id: string; url: string } {\n if (input.id) {\n if (!DIGITS_ONLY.test(input.id)) {\n throw invalidInput(\n `\"${input.id}\" is not a lyrics.com song id.`,\n \"Ids are digits only, as returned by the search tools.\",\n );\n }\n return { id: input.id, url: buildSongUrl(input.id) };\n }\n\n if (input.url) {\n if (!isLyricsComHost(input.url)) {\n throw invalidInput(\n \"Only www.lyrics.com URLs are accepted.\",\n \"Pass the source_url returned by search_lyrics or search_songs, or use the numeric id instead.\",\n );\n }\n const id = extractSongId(input.url);\n if (!id) {\n throw invalidInput(\n \"That lyrics.com URL is not a song page.\",\n \"Song pages look like https://www.lyrics.com/lyric-lf/1234567/Artist/Title\",\n );\n }\n return { id, url: input.url };\n }\n\n throw invalidInput(\"Either 'id' or 'url' must be provided.\");\n}\n\n/** Absolute URL for a href scraped out of a result row. */\nexport function toAbsoluteUrl(href: string): string {\n return new URL(href, BASE_URL).toString();\n}\n","/**\n * Every lyrics.com CSS selector lives here, so a site redesign is a one-file fix.\n *\n * Selectors deliberately target a single stable class rather than a full class\n * list (`div.sec-lyric`, not `div.sec-lyric.clearfix`), so that a layout tweak\n * adding or removing a utility class does not break extraction.\n */\n\nexport const SEL = {\n /** One search result row. */\n serpItem: \"div.sec-lyric\",\n /**\n * Page chrome that is present whether or not there are results. Used to tell\n * \"zero results\" apart from \"the markup changed\", which must not look alike.\n */\n serpShell: \"#lyric-search, .lyric-search, .pagination, #main-content, form[action*='serp']\",\n resultTitle: \"p.lyric-meta-title a\",\n /**\n * Rows tied to an album expose the artist under `lyric-meta-album-artist`,\n * rows without one under `lyric-meta-artists`. Both shapes appear on the same\n * page, so missing the second silently drops results.\n */\n resultArtist: \"p.lyric-meta-album-artist a, p.lyric-meta-artists a\",\n resultAlbum: \"p.lyric-meta-album\",\n resultYear: \"p.lyric-meta-album-year\",\n resultSnippet: \"pre.lyric-body\",\n\n /** Full lyrics on a song page, with the historical fallback. */\n songBody: \"pre#lyric-body-text, pre.lyric-body\",\n /** Marker for a valid song page that simply has no lyrics on file. */\n songNoData: \"div.lyric-no-data\",\n songTitle: \"#lyric-title-text, .lyric-title\",\n songArtist: \".lyric-artist a, .lyric-artist\",\n} as const;\n","/**\n * Parser for a lyrics.com song page.\n */\n\nimport { load } from \"cheerio\";\nimport type { CheerioAPI } from \"cheerio\";\nimport { parseFailure } from \"../../errors.js\";\nimport type { SongPage } from \"../../types.js\";\nimport { cleanInlineText, cleanLyricsText } from \"../../text/normalize.js\";\nimport { SEL } from \"./selectors.js\";\n\n/** The wording the site puts in the title of a page holding no words. */\nconst NO_LYRICS_FOUND = /no lyrics found/i;\n\nexport interface ParseSongOptions {\n id: string;\n url: string;\n}\n\nexport function parseSongPage(html: string, options: ParseSongOptions): SongPage {\n const $ = load(html);\n\n const title = cleanInlineText($(SEL.songTitle).first().text()) || null;\n const artist = cleanInlineText($(SEL.songArtist).first().text()) || null;\n\n const body = $(SEL.songBody).first();\n if (body.length === 0) {\n // A valid song page can legitimately carry no lyrics. That is an answer,\n // not a failure, so it is only treated as breakage when the page says\n // nothing about why the text is missing.\n const declaresNoLyrics =\n $(SEL.songNoData).length > 0 || NO_LYRICS_FOUND.test($(\"title\").text());\n if (!declaresNoLyrics) {\n throw parseFailure(options.url, \"no lyrics container and no 'no lyrics' marker\");\n }\n return { id: options.id, url: options.url, title, artist, lyrics: \"\", hasLyrics: false };\n }\n\n const lyrics = cleanLyricsText(extractBodyText($, body));\n if (!lyrics) {\n return { id: options.id, url: options.url, title, artist, lyrics: \"\", hasLyrics: false };\n }\n\n return { id: options.id, url: options.url, title, artist, lyrics, hasLyrics: true };\n}\n\n/**\n * lyrics.com wraps many individual words in links to its dictionary pages.\n * Those anchors are unwrapped in the raw HTML rather than through the DOM,\n * because DOM-level removal loses the line breaks that give lyrics their shape.\n */\nfunction extractBodyText($: CheerioAPI, body: ReturnType<CheerioAPI>): string {\n const rawHtml = $.html(body);\n const withoutAnchors = rawHtml.replace(/<a[^>]*>([\\s\\S]*?)<\\/a>/gi, \"$1\");\n const withNewlines = withoutAnchors.replace(/<br\\s*\\/?>/gi, \"\\n\");\n return load(withNewlines).root().text();\n}\n","/**\n * High-level lyrics.com client.\n *\n * This module knows nothing about MCP, which keeps it unit-testable against\n * plain strings and usable as a plain library through the `./client` export.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport {\n DEFAULT_USER_AGENT,\n MIN_ALLOWED_INTERVAL_MS,\n createLogger,\n loadConfig,\n} from \"../config.js\";\nimport type { SearchPage, SongPage } from \"../types.js\";\nimport { TtlLruCache } from \"./cache.js\";\nimport { fetchHtml } from \"./http.js\";\nimport { parseSearchResults } from \"./parsers/searchResults.js\";\nimport { parseSongPage } from \"./parsers/songPage.js\";\nimport { RateLimiter } from \"./rateLimiter.js\";\nimport { buildSearchUrl, resolveSongRef } from \"./urls.js\";\n\n/** The names a User-Agent carries when it passes traffic off as a browser. */\nconst BROWSER_IDENTITY = /mozilla\\/|applewebkit|chrome\\/|safari\\/|gecko/i;\n\nexport interface LyricsComClientOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\nexport interface FetchOutcome<T> {\n data: T;\n /** True when the page came from the in-memory cache rather than the network. */\n cached: boolean;\n}\n\n/**\n * Apply the guarantees this project makes about its own traffic.\n *\n * The environment parser already enforces both, but `LyricsComClient` is published as a\n * library through the `./client` export and takes a caller-built config, so\n * without this the pacing floor and the honest identity are optional for anyone\n * importing it. lyrics.com answers a burst by refusing, and an honest User-Agent is what it accepts, and those promises hold on every path.\n *\n * A caller may still name their own application in the User-Agent. Passing the\n * traffic off as a browser is a different thing, and gets the project's own\n * identity appended so it stays attributable.\n */\nfunction withGuarantees(config: Config): Config {\n const userAgent = BROWSER_IDENTITY.test(config.userAgent)\n ? `${config.userAgent} ${DEFAULT_USER_AGENT}`\n : config.userAgent;\n return {\n ...config,\n userAgent,\n minIntervalMs: Math.max(MIN_ALLOWED_INTERVAL_MS, config.minIntervalMs),\n };\n}\n\nexport class LyricsComClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly limiter: RateLimiter;\n private readonly cache: TtlLruCache<unknown>;\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: LyricsComClientOptions = {}) {\n this.config = withGuarantees(options.config ?? loadConfig());\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.limiter = new RateLimiter({ minIntervalMs: this.config.minIntervalMs });\n this.cache = new TtlLruCache<unknown>(this.config.cacheMaxEntries, this.config.cacheTtlMs);\n this.fetchImpl = options.fetchImpl;\n }\n\n async search(term: string, page: number): Promise<FetchOutcome<SearchPage>> {\n const url = buildSearchUrl(term, page);\n return await this.fetchParsed(url, (html) => parseSearchResults(html, { page, url }));\n }\n\n async getSong(ref: { id?: string; url?: string }): Promise<FetchOutcome<SongPage>> {\n const { id, url } = resolveSongRef(ref);\n return await this.fetchParsed(url, (html) => parseSongPage(html, { id, url }));\n }\n\n /**\n * Fetch, parse, then cache. In that order: a page that could not be read is\n * never stored, so a bad minute at lyrics.com cannot be replayed from memory for\n * the rest of the cache lifetime, leaving the tool unable to recover after\n * the site comes back.\n *\n * The cached value is the parsed result rather than the raw page, which also\n * keeps a few hundred kilobytes of markup per entry out of memory.\n */\n private async fetchParsed<T>(url: string, parse: (html: string) => T): Promise<FetchOutcome<T>> {\n const hit = this.cache.get(url);\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${url}`);\n return { data: hit as T, cached: true };\n }\n\n const html = await fetchHtml(url, {\n config: this.config,\n limiter: this.limiter,\n logger: this.logger,\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n });\n\n const data = parse(html);\n this.cache.set(url, data);\n return { data, cached: false };\n }\n}\n","/**\n * get_lyrics: fetch the full lyrics of one song.\n */\n\nimport { z } from \"zod\";\nimport type { LyricsComClient } from \"../lyricscom/client.js\";\nimport { findMatchingLine } from \"../text/excerpt.js\";\nimport { strictInput } from \"./arguments.js\";\nimport { ok, toToolError, truncate } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\n/** Where the answer stopped in the words, and what to ask for to go on. */\nfunction howFarTheTextGoes(pastTheEnd: boolean, offset: number, nextOffset: number | null): string {\n if (pastTheEnd) {\n return `Nothing at offset=${offset}: that is past the end. Call again with offset=0.`;\n }\n if (nextOffset === null) {\n return \"Complete.\";\n }\n return `Truncated: call again with offset=${nextOffset} to continue.`;\n}\n\n/** What was found of the word a caller asked to be shown, when they asked for one. */\nfunction whereTheWordIs(\n highlight: { word: string; found: boolean; line_number: number | null } | null | undefined,\n): string {\n if (!highlight) {\n return \"\";\n }\n if (!highlight.found) {\n return `\"${highlight.word}\" does not appear in these lyrics.`;\n }\n return `\"${highlight.word}\" appears on line ${highlight.line_number}.`;\n}\n\nexport const getLyricsDescription = [\n \"Fetch the full lyrics of one song from lyrics.com, given the song id or URL returned by search_lyrics or search_songs.\",\n \"Lyrics can be long, so the response is truncated by default: check 'truncated' and call again with 'offset' set to\",\n \"'next_offset' to continue reading.\",\n 'Some lyrics.com pages legitimately have no lyrics on file; those come back with status \"no_lyrics\" and an empty',\n \"lyrics field, which is a valid answer and not an error, so do not retry them.\",\n \"Always cite 'source_url' and the artist when showing lyrics to a user.\",\n].join(\" \");\n\nexport const getLyricsInput = strictInput({\n id: z\n .string()\n .regex(/^\\d+$/, \"Song ids are digits only.\")\n .optional()\n .describe(\"lyrics.com numeric song id, as returned by the search tools. Preferred over 'url'.\"),\n url: z\n .string()\n .url()\n .optional()\n .describe(\n \"Full lyrics.com song URL. Only www.lyrics.com URLs are accepted. Ignored when 'id' is given.\",\n ),\n max_chars: z\n .number()\n .int()\n .min(200)\n .max(20_000)\n .default(6000)\n .describe(\"Maximum characters of lyrics text to return in this call.\"),\n offset: z\n .number()\n .int()\n .min(0)\n .default(0)\n .describe(\"Character offset to resume from, for lyrics longer than max_chars.\"),\n highlight: z\n .string()\n .max(120)\n .optional()\n .describe(\n \"Optional word. When set, the response reports whether it appears in the lyrics and on which line.\",\n ),\n});\n\nexport const getLyricsOutputShape = {\n status: z.enum([\"ok\", \"no_lyrics\"]),\n id: z.string(),\n title: z.string().nullable(),\n artist: z.string().nullable(),\n source_url: z.string(),\n lyrics: z.string().describe(\"Slice of the lyrics text. Empty when status is 'no_lyrics'.\"),\n total_chars: z.number().int(),\n returned_chars: z.number().int(),\n offset: z.number().int(),\n next_offset: z.number().int().nullable(),\n truncated: z.boolean(),\n line_count: z.number().int(),\n highlight: z\n .object({\n word: z.string(),\n found: z.boolean(),\n line_number: z.number().int().nullable(),\n line: z.string().nullable(),\n })\n .nullable(),\n attribution: z.string().describe(\"Ready-to-display credit line.\"),\n source: z.literal(\"lyrics.com\"),\n notes: z.array(z.string()),\n};\n\nexport interface GetLyricsArgs {\n id?: string;\n url?: string;\n max_chars: number;\n offset: number;\n highlight?: string;\n}\n\n/**\n * Cut at the last newline that fits, so a continuation never splits a line in\n * half. Falls back to a hard cut for a single line longer than the budget.\n */\nexport function sliceAtLineBoundary(\n text: string,\n offset: number,\n maxChars: number,\n): { slice: string; nextOffset: number | null } {\n if (offset >= text.length) {\n return { slice: \"\", nextOffset: null };\n }\n\n const remaining = text.slice(offset);\n if (remaining.length <= maxChars) {\n return { slice: remaining, nextOffset: null };\n }\n\n const window = remaining.slice(0, maxChars);\n const lastBreak = window.lastIndexOf(\"\\n\");\n const cut = lastBreak > 0 ? lastBreak : maxChars;\n return { slice: remaining.slice(0, cut), nextOffset: offset + cut };\n}\n\nexport async function runGetLyrics(\n client: LyricsComClient,\n args: GetLyricsArgs,\n): Promise<ToolResult> {\n try {\n const ref: { id?: string; url?: string } = {};\n if (args.id) {\n ref.id = args.id;\n } else if (args.url) {\n ref.url = args.url;\n }\n\n const { data, cached } = await client.getSong(ref);\n const notes: string[] = [];\n if (cached) {\n notes.push(\"Served from this server's short-lived in-memory cache.\");\n }\n const credit = [data.title, data.artist].filter(Boolean).join(\" — \");\n const attribution = `${credit || \"Lyrics\"} via lyrics.com — ${data.url}`;\n\n if (!data.hasLyrics) {\n const structured = {\n status: \"no_lyrics\" as const,\n id: data.id,\n title: data.title,\n artist: data.artist,\n source_url: data.url,\n lyrics: \"\",\n total_chars: 0,\n returned_chars: 0,\n offset: 0,\n next_offset: null,\n truncated: false,\n notes,\n line_count: 0,\n highlight: null,\n attribution,\n source: \"lyrics.com\" as const,\n };\n return ok(\n structured,\n `lyrics.com has a page for this song but no lyrics on file.\\n${attribution}`,\n );\n }\n\n const { slice, nextOffset } = sliceAtLineBoundary(data.lyrics, args.offset, args.max_chars);\n const match = args.highlight ? findMatchingLine(data.lyrics, args.highlight) : null;\n\n const structured = {\n status: \"ok\" as const,\n id: data.id,\n title: data.title,\n artist: data.artist,\n source_url: data.url,\n lyrics: slice,\n total_chars: data.lyrics.length,\n returned_chars: slice.length,\n offset: args.offset,\n next_offset: nextOffset,\n truncated: nextOffset !== null,\n line_count: data.lyrics === \"\" ? 0 : data.lyrics.split(\"\\n\").length,\n highlight: args.highlight\n ? {\n word: args.highlight,\n found: match !== null,\n line_number: match?.lineNumber ?? null,\n line: match?.line ?? null,\n }\n : null,\n attribution,\n source: \"lyrics.com\" as const,\n notes,\n };\n\n // An offset beyond the words yields an empty body, which on its own reads\n // as a song that carries none. What happened is that the caller asked for\n // a position that does not exist, and only saying so tells the two apart.\n const pastTheEnd = args.offset > 0 && slice === \"\" && data.lyrics.length > 0;\n if (pastTheEnd) {\n notes.push(\n `offset=${args.offset} is past the end of a body of ${data.lyrics.length} characters. Call again with offset=0 to read it from the start.`,\n );\n }\n\n // The text mirror stays a pointer to the structured payload rather than a\n // second copy of the lyrics.\n const summary = [\n attribution,\n `${structured.line_count} lines, ${structured.total_chars} characters.`,\n howFarTheTextGoes(pastTheEnd, args.offset, nextOffset),\n whereTheWordIs(structured.highlight),\n ]\n .filter(Boolean)\n .join(\"\\n\");\n\n return ok(structured, truncate(`${summary}\\n\\n${slice}`, 4000), notes);\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * Two-pass keyword matcher.\n *\n * lyrics.com's own search is fuzzy: it returns title-only matches and loose\n * matches alongside genuine lyric-body hits. This matcher is what lets the\n * server promise that a returned song really contains the query.\n *\n * Pass 1 requires the keyword to be delimited on both sides, so \"coup\" does not\n * match \"beaucoup\". Pass 2 relaxes the right-hand side to a word start, so\n * \"enfant\" still matches \"enfants\". Pass 2 only runs when pass 1 finds nothing,\n * which keeps exact hits ranked ahead of prefix hits.\n */\n\n/**\n * Characters treated as word boundaries. Includes the typographic apostrophe,\n * which is common in French lyrics (\"l’enfant\").\n */\nconst WORD_DELIMITERS = \"[\\\\s,;:'\\\".!?()\\\\[\\\\]&’`@-]\";\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Index of `keyword` inside `text`, or -1 when absent.\n * The returned index points at the keyword itself, not at the leading delimiter.\n */\nexport function getKeywordIndex(keyword: string, text: string): number {\n if (!(keyword && text)) {\n return -1;\n }\n\n const escaped = escapeRegExp(keyword.toLowerCase());\n const haystack = text.toLowerCase();\n\n const passes = [\n new RegExp(`(^|${WORD_DELIMITERS})${escaped}(${WORD_DELIMITERS}|$)`),\n new RegExp(`(^|${WORD_DELIMITERS})${escaped}`),\n ];\n\n for (const pattern of passes) {\n const match = pattern.exec(haystack);\n if (match) {\n const leadingDelimiter = match[1] ?? \"\";\n return match.index + leadingDelimiter.length;\n }\n }\n\n return -1;\n}\n\nexport function containsWord(keyword: string, text: string): boolean {\n return getKeywordIndex(keyword, text) !== -1;\n}\n","/**\n * Excerpt extraction.\n *\n * Search results carry one short line showing where the query appears, never a\n * lyrics block. Excerpts stay small on purpose: they are enough for a model to\n * pick the right song, and search results are not the place to reproduce a\n * song's text.\n */\n\nimport { getKeywordIndex } from \"./keywordIndex.js\";\n\nexport interface ExcerptOptions {\n /** Hard cap on the returned excerpt. */\n maxChars?: number;\n}\n\nexport interface LineMatch {\n line: string;\n /** 1-based line number within the text. */\n lineNumber: number;\n}\n\n/** First line containing `keyword`, using the two-pass matcher. */\nexport function findMatchingLine(text: string, keyword: string): LineMatch | null {\n if (!(text && keyword)) {\n return null;\n }\n const lines = text.split(\"\\n\");\n for (let i = 0; i < lines.length; i += 1) {\n const line = lines[i] ?? \"\";\n if (line.trim() === \"\") {\n continue;\n }\n if (getKeywordIndex(keyword, line) !== -1) {\n return { line: line.trim(), lineNumber: i + 1 };\n }\n }\n return null;\n}\n\n/**\n * Excerpt of the line where `keyword` appears, trimmed to a window centred on\n * the match. Falls back to the first non-empty line when the keyword is absent.\n */\nexport function matchedLineExcerpt(\n text: string,\n keyword: string,\n options: ExcerptOptions = {},\n): string | null {\n const maxChars = options.maxChars ?? 160;\n if (!text) {\n return null;\n }\n\n const match = findMatchingLine(text, keyword);\n const line =\n match?.line ??\n text\n .split(\"\\n\")\n .find((candidate) => candidate.trim() !== \"\")\n ?.trim();\n if (!line) {\n return null;\n }\n if (line.length <= maxChars) {\n return line;\n }\n\n const keywordAt = match ? Math.max(getKeywordIndex(keyword, line), 0) : 0;\n const start = Math.max(0, keywordAt - Math.floor(maxChars / 2));\n const end = Math.min(line.length, start + maxChars);\n const slice = line.slice(start, end).trim();\n return `${start > 0 ? \"…\" : \"\"}${slice}${end < line.length ? \"…\" : \"\"}`;\n}\n","/**\n * How a tool's arguments are declared, and what happens to one that is not.\n *\n * An argument this server does not declare is a question it cannot answer.\n * Reading it and dropping it produces an answer computed on the defaults, which\n * a caller reads as the answer to what they asked. So an undeclared argument is\n * refused, and the refusal names it and offers the declared name when one is\n * close enough to be the one that was meant.\n *\n * A caller branches on the code a refusal opens with, and arguments are refused\n * along two paths: the code of a tool writes its own refusals, and the schema\n * refuses on its own before that code runs. Both open with the same code here,\n * so a caller reading the vocabulary finds it whichever path refused.\n */\n\nimport { z } from \"zod\";\n\n/** The code a caller branches on when the arguments cannot produce a request. */\nconst INVALID_INPUT = \"invalid_input\";\n\n/** Zod's own wording. The error code is written in front of it. */\nconst defaultMessage = z.core.locales.en().localeError;\n\n/** Declare a tool's arguments, refusing anything outside the declaration. */\nexport function strictInput<Shape extends z.ZodRawShape>(shape: Shape) {\n const declared = Object.keys(shape);\n\n const refuse: z.core.$ZodErrorMap = (issue) =>\n issue.code === \"unrecognized_keys\"\n ? unknownArgumentMessage(issue.keys, declared)\n : `[${INVALID_INPUT}] ${defaultMessage(issue)}`;\n\n for (const argument of Object.values(shape)) {\n carryTheCode(argument, refuse);\n }\n\n return z.strictObject(shape, { error: refuse });\n}\n\n/**\n * Route every refusal a declared argument can raise through one error map.\n *\n * A bound such as `min` raises its issue from the check that holds it, and the\n * issue names that check as the instance whose error map applies, so the map\n * reaches it only by being written onto the check itself. Zod offers no copy\n * that keeps a check's identity, hence the assignment. It is confined to\n * schemas built for a declaration and handed to this function, which is the\n * whole of what a tool module does with them.\n */\nfunction carryTheCode(schema: z.core.$ZodType, refuse: z.core.$ZodErrorMap): void {\n const definition = schema._zod.def as {\n error?: unknown;\n checks?: Array<{ _zod: { def: { error?: unknown } } }>;\n innerType?: z.core.$ZodType;\n };\n\n definition.error = refuse;\n for (const check of definition.checks ?? []) {\n check._zod.def.error = refuse;\n }\n\n // A default, an optional or any other wrapper answers for its own shape and\n // leaves the bounds to the type it wraps.\n if (definition.innerType) {\n carryTheCode(definition.innerType, refuse);\n }\n}\n\nfunction unknownArgumentMessage(keys: readonly string[], declared: readonly string[]): string {\n const named = keys\n .map((key) => {\n const near = nearestArgument(key, declared);\n return near ? `'${key}' (did you mean '${near}'?)` : `'${key}'`;\n })\n .join(\", \");\n\n return (\n `[${INVALID_INPUT}] Unknown ${keys.length > 1 ? \"arguments\" : \"argument\"} ${named}. ` +\n `This tool takes: ${declared.join(\", \")}.`\n );\n}\n\n/**\n * The declared name a caller most plausibly meant, when there is one.\n *\n * Three readings, ordered by how much each claims: the same name written\n * differently, a name that opens or closes the other, and a name a couple of\n * typing slips away. Anything further is left unnamed, because a suggestion\n * that misses sends a caller to an argument answering a different question.\n */\nfunction nearestArgument(key: string, declared: readonly string[]): string | undefined {\n const flatten = (name: string) => name.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n const flat = flatten(key);\n if (flat.length === 0) {\n return undefined;\n }\n\n const sameName = declared.find((name) => flatten(name) === flat);\n if (sameName) {\n return sameName;\n }\n\n // Either name may be the longer one: a caller can qualify a name this tool\n // keeps plain, or shorten one it spells out.\n const overlapping = declared.find((name) => {\n const other = flatten(name);\n const [shorter, longer] = other.length < flat.length ? [other, flat] : [flat, other];\n // Two characters in common say nothing; three start to.\n return shorter.length >= 3 && (longer.startsWith(shorter) || longer.endsWith(shorter));\n });\n if (overlapping) {\n return overlapping;\n }\n\n let closest: string | undefined;\n let shortest = Number.POSITIVE_INFINITY;\n for (const name of declared) {\n const distance = editDistance(flat, flatten(name));\n if (distance < shortest) {\n shortest = distance;\n closest = name;\n }\n }\n\n // Up to a third of the name may differ. Past that the match is a guess.\n return shortest <= Math.max(1, Math.floor(flat.length / 3)) ? closest : undefined;\n}\n\n/**\n * Single-character insertions, deletions and substitutions between two words.\n *\n * One row of the matrix is kept at a time. The cell above comes from walking\n * that row, and the two other neighbours a cell needs, the one to its left and\n * the one diagonally above it, are carried forward from the step before, so\n * every value the walk uses is one it has just computed.\n */\nfunction editDistance(left: string, right: string): number {\n let previous = Array.from({ length: right.length + 1 }, (_, index) => index);\n let distance = right.length;\n\n for (let row = 1; row <= left.length; row += 1) {\n const current: number[] = [row];\n let diagonal = row - 1;\n let leftward = row;\n\n for (const [column, above] of previous.entries()) {\n if (column === 0) {\n continue;\n }\n const substitution = diagonal + (left[row - 1] === right[column - 1] ? 0 : 1);\n const cell = Math.min(substitution, above + 1, leftward + 1);\n current.push(cell);\n diagonal = above;\n leftward = cell;\n }\n\n previous = current;\n distance = leftward;\n }\n\n return distance;\n}\n","/**\n * Pieces shared by the three tools: result schema, error mapping, text mirrors.\n */\n\nimport { z } from \"zod\";\nimport { LyricsComError } from \"../errors.js\";\nimport type { SongResult } from \"../types.js\";\n\n/** Many MCP clients render only the text block, so it must stay readable on its own. */\nexport const MAX_TEXT_MIRROR_CHARS = 1500;\n\n/**\n * The last page a search will accept.\n *\n * Offering a page beyond this sends a caller into an argument the schema\n * refuses, so the number that bounds the input is the number an answer is\n * allowed to suggest.\n */\nexport const MAX_PAGE = 20;\n\nexport const songResultSchema = z.object({\n id: z.string().describe(\"lyrics.com song id. Pass this to get_lyrics.\"),\n title: z.string(),\n artist: z.string(),\n album: z.string().nullable(),\n year: z.number().int().nullable(),\n source_url: z.string().describe(\"Canonical lyrics.com page for this song.\"),\n excerpt: z\n .string()\n .nullable()\n .describe(\"Short lyric line showing where the query appears, when available.\"),\n});\n\nexport type SongResultOut = z.infer<typeof songResultSchema>;\n\nexport function toSongResultOut(song: SongResult, excerpt: string | null): SongResultOut {\n return {\n id: song.id,\n title: song.title,\n artist: song.artist,\n album: song.album,\n year: song.year,\n source_url: song.url,\n excerpt,\n };\n}\n\nexport interface ToolResult {\n // The SDK's CallToolResult carries an index signature for protocol extensions.\n [key: string]: unknown;\n content: Array<{ type: \"text\"; text: string }>;\n structuredContent?: Record<string, unknown>;\n isError?: boolean;\n}\n\n/**\n * Keep text from the site out of the shape this server's own lines take.\n *\n * A block ending with lines that open \"Note:\" gives a caller no way to tell one\n * of those from the same words inside a title someone else published.\n * Indenting such a line in the body keeps the two apart, and the structured\n * output still carries it exactly as it was.\n */\nfunction indentMarkerLines(body: string): string {\n return body.replace(/^(Note:)/gm, \" $1\");\n}\n\n/**\n * Build a result whose text block ends with its notes.\n *\n * The notes are what qualifies an answer: that an offset landed past the end,\n * that the site chose these results rather than matching them, that the words\n * came from this server's cache. A client rendering only the text reads an\n * unqualified answer without them.\n */\nexport function ok(\n structured: Record<string, unknown>,\n text: string,\n notes: string[] = [],\n): ToolResult {\n const trailer = notes.map((note) => `Note: ${note}`).join(\"\\n\");\n const budget = MAX_TEXT_MIRROR_CHARS - (trailer ? trailer.length + 2 : 0);\n const body = truncate(indentMarkerLines(text), Math.max(0, budget));\n\n return {\n content: [{ type: \"text\", text: trailer ? `${body}\\n\\n${trailer}` : body }],\n structuredContent: structured,\n };\n}\n\n/**\n * Error results carry no structuredContent: the SDK validates it against the\n * tool's declared output schema, which an error payload does not satisfy.\n */\nexport function toToolError(error: unknown): ToolResult {\n const known =\n error instanceof LyricsComError\n ? error\n : new LyricsComError(\"network_error\", error instanceof Error ? error.message : String(error));\n\n const lines = [`[${known.code}] ${known.message}`];\n if (known.details.hint) {\n lines.push(`Hint: ${known.details.hint}`);\n }\n if (known.details.url) {\n lines.push(`URL: ${known.details.url}`);\n }\n\n return { content: [{ type: \"text\", text: lines.join(\"\\n\") }], isError: true };\n}\n\nexport function truncate(text: string, maxChars: number): string {\n if (text.length <= maxChars) {\n return text;\n }\n return `${text.slice(0, maxChars - 1).trimEnd()}…`;\n}\n\n/** Compact listing used by both search tools. */\nexport function renderResultList(results: SongResultOut[]): string {\n return results\n .map((result, index) => {\n const year = result.year ? ` (${result.year})` : \"\";\n const album = result.album ? ` [${result.album}]` : \"\";\n const head = `${index + 1}. ${result.title} — ${result.artist}${year}${album} · id: ${result.id}`;\n return result.excerpt ? `${head}\\n \"${result.excerpt}\"` : head;\n })\n .join(\"\\n\");\n}\n","/**\n * search_lyrics: find songs by a word or phrase occurring in the lyrics.\n */\n\nimport { z } from \"zod\";\nimport type { LyricsComClient } from \"../lyricscom/client.js\";\nimport { dedupeSongs } from \"../text/dedupe.js\";\nimport { matchedLineExcerpt } from \"../text/excerpt.js\";\nimport { containsWord } from \"../text/keywordIndex.js\";\nimport type { SongResult } from \"../types.js\";\nimport { strictInput } from \"./arguments.js\";\nimport {\n MAX_PAGE,\n ok,\n renderResultList,\n songResultSchema,\n toSongResultOut,\n toToolError,\n} from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\n/** Verifying against full lyrics costs one page fetch per candidate. */\nconst MAX_FULL_VERIFICATIONS = 5;\n\n/**\n * The rows whose words actually carry the query, and how many did not.\n *\n * lyrics.com answers a search with rows whose snippet may not hold the words at\n * all, so each row is checked against the text there is: its snippet, or the\n * full lyrics when a caller asked for that and the cap allows another read. A\n * single unreadable song page is noted and stepped over rather than sinking the\n * whole search.\n */\nasync function keepTheOnesCarryingTheWords(\n client: LyricsComClient,\n args: SearchLyricsArgs,\n unique: readonly SongResult[],\n notes: string[],\n): Promise<{ kept: Array<{ song: SongResult; excerpt: string | null }>; filteredOut: number }> {\n const kept: Array<{ song: SongResult; excerpt: string | null }> = [];\n let filteredOut = 0;\n let fullChecks = 0;\n\n for (const song of unique) {\n if (kept.length >= args.limit) {\n break;\n }\n\n let text = song.snippet ?? \"\";\n if (args.verify === \"full\" && fullChecks < MAX_FULL_VERIFICATIONS) {\n fullChecks += 1;\n try {\n const page = await client.getSong({ id: song.id });\n if (page.data.hasLyrics) {\n text = page.data.lyrics;\n }\n } catch {\n notes.push(`Could not verify \"${song.title}\" against its full lyrics.`);\n }\n }\n\n const matches = args.verify === \"none\" || (text !== \"\" && containsWord(args.query, text));\n if (!matches) {\n filteredOut += 1;\n continue;\n }\n\n const excerpt =\n args.include_excerpt && text ? matchedLineExcerpt(text, args.query, { maxChars: 160 }) : null;\n kept.push({ song, excerpt });\n }\n\n return { kept, filteredOut };\n}\n\n/**\n * What to say about the rows past this page.\n *\n * The sentence may only name a page the schema would accept, or it sends a\n * caller into an argument that is refused.\n */\nfunction whatFollowsThisPage(hasMore: boolean, page: number): string {\n if (!hasMore) {\n return \"\";\n }\n if (page < MAX_PAGE) {\n return `\\n\\nMore results available: call again with page=${page + 1}.`;\n }\n return `\\n\\nlyrics.com holds more, but page ${MAX_PAGE} is as far as this tool reads. Narrow the query instead.`;\n}\n\nexport const searchLyricsDescription = [\n \"Search lyrics.com for songs whose lyrics contain a given word or phrase.\",\n \"Returns matching songs with artist, title, a short excerpt of the line where the word appears,\",\n \"and the lyrics.com id and URL needed to fetch the full text with get_lyrics.\",\n \"Use this when someone remembers a fragment of lyrics but not the song, or wants songs mentioning a word or theme.\",\n \"One call returns one page of lyrics.com results; to see more, increase 'page' rather than 'limit'.\",\n \"lyrics.com also returns loose and title-only matches, so results are filtered locally to keep only songs\",\n \"where the word genuinely appears in the lyrics. Set 'verify' to \\\"none\\\" to see the raw, unfiltered list.\",\n].join(\" \");\n\nexport const searchLyricsInput = strictInput({\n query: z\n .string()\n .min(1)\n .max(120)\n .describe(\"Word or short phrase to look for inside the lyrics, for example 'autrefois'.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(50)\n .default(10)\n .describe(\"Maximum songs to return from this page.\"),\n page: z\n .number()\n .int()\n .min(1)\n .max(20)\n .default(1)\n .describe(\"Result page on lyrics.com. Each page holds up to 24 raw results.\"),\n verify: z\n .enum([\"snippet\", \"full\", \"none\"])\n .default(\"snippet\")\n .describe(\n \"How to confirm the query really appears in the lyrics. 'snippet' checks the excerpt lyrics.com already \" +\n \"returned and costs nothing. 'full' fetches up to 5 song pages and checks the complete lyrics, which is \" +\n \"slow and can trigger rate limiting. 'none' disables filtering.\",\n ),\n include_excerpt: z\n .boolean()\n .default(true)\n .describe(\"Include the matching lyric line excerpt in each result.\"),\n});\n\nexport const searchLyricsOutputShape = {\n query: z.string(),\n page: z.number().int(),\n results: z.array(songResultSchema),\n raw_result_count: z\n .number()\n .int()\n .describe(\"Rows lyrics.com returned before local filtering and deduplication.\"),\n filtered_out: z\n .number()\n .int()\n .describe(\"Rows dropped because the query was not found in their lyrics.\"),\n has_more: z.boolean(),\n next_page: z.number().int().nullable(),\n source: z.literal(\"lyrics.com\"),\n notes: z\n .array(z.string())\n .describe(\"Caveats worth knowing, such as a cache hit or partial verification.\"),\n};\n\nexport interface SearchLyricsArgs {\n query: string;\n limit: number;\n page: number;\n verify: \"snippet\" | \"full\" | \"none\";\n include_excerpt: boolean;\n}\n\nexport async function runSearchLyrics(\n client: LyricsComClient,\n args: SearchLyricsArgs,\n): Promise<ToolResult> {\n try {\n const { data, cached } = await client.search(args.query, args.page);\n const notes: string[] = [];\n if (cached) {\n notes.push(\"Served from this server's short-lived in-memory cache.\");\n }\n\n const unique = dedupeSongs(data.results);\n const { kept, filteredOut } = await keepTheOnesCarryingTheWords(client, args, unique, notes);\n\n if (args.verify === \"full\" && unique.length > MAX_FULL_VERIFICATIONS) {\n notes.push(\n `Full verification is capped at ${MAX_FULL_VERIFICATIONS} songs per call to avoid rate limiting; ` +\n \"the remaining results were checked against their search excerpt.\",\n );\n }\n if (kept.length === 0 && data.rawCount > 0) {\n notes.push(\n \"lyrics.com returned results but none of them actually contain the query in their lyrics. \" +\n 'Try verify=\"none\" to see the raw matches.',\n );\n }\n\n const results = kept.map((entry) => toSongResultOut(entry.song, entry.excerpt));\n const structured = {\n query: args.query,\n page: args.page,\n results,\n raw_result_count: data.rawCount,\n filtered_out: filteredOut,\n has_more: data.hasMore,\n next_page: data.hasMore && args.page < MAX_PAGE ? args.page + 1 : null,\n source: \"lyrics.com\" as const,\n notes,\n };\n\n const header =\n results.length > 0\n ? `${results.length} song(s) on page ${args.page} for \"${args.query}\":`\n : `No song on page ${args.page} matched \"${args.query}\".`;\n // The footer may only name a page the schema would accept, or it sends a\n // caller into an argument that is refused.\n const footer = whatFollowsThisPage(data.hasMore, args.page);\n\n return ok(structured, `${header}\\n${renderResultList(results)}${footer}`);\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * Deduplication of search results.\n *\n * lyrics.com lists the same recording once per album it appears on, so a single\n * query routinely returns \"Title\", \"Title [Deluxe]\" and \"Title (Remastered)\" by\n * the same artist. Bracketed and parenthesised suffixes are stripped before\n * comparison so those collapse into one result.\n */\n\nimport type { SongResult } from \"../types.js\";\n\nexport function songKey(artist: string, title: string): string {\n const withoutSuffixes = title.replace(/\\s*[[(].*?[\\])]/g, \"\");\n const titleNorm = withoutSuffixes.replace(/\\s+/g, \" \").trim().toLowerCase();\n const artistNorm = artist.replace(/\\s+/g, \" \").trim().toLowerCase();\n return `${artistNorm}|${titleNorm}`;\n}\n\n/** Keep the first occurrence of each artist/title pair, preserving order. */\nexport function dedupeSongs<T extends SongResult>(songs: T[]): T[] {\n const seen = new Set<string>();\n const unique: T[] = [];\n for (const song of songs) {\n const key = songKey(song.artist, song.title);\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n unique.push(song);\n }\n return unique;\n}\n","/**\n * search_songs: find songs by title.\n *\n * lyrics.com has no title-only endpoint, so this runs the same search and ranks\n * and filters the rows by how well their title matches. The tool description\n * says so plainly rather than implying a precision the site does not offer.\n */\n\nimport { z } from \"zod\";\nimport type { LyricsComClient } from \"../lyricscom/client.js\";\nimport { dedupeSongs } from \"../text/dedupe.js\";\nimport type { SongResult } from \"../types.js\";\nimport { strictInput } from \"./arguments.js\";\nimport {\n MAX_PAGE,\n ok,\n renderResultList,\n songResultSchema,\n toSongResultOut,\n toToolError,\n} from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\n/**\n * What to say about the rows past this page.\n *\n * The sentence may only name a page the schema would accept, or it sends a\n * caller into an argument that is refused.\n */\nfunction whatFollowsThisPage(hasMore: boolean, page: number): string {\n if (!hasMore) {\n return \"\";\n }\n if (page < MAX_PAGE) {\n return `\\n\\nMore results available: call again with page=${page + 1}.`;\n }\n return `\\n\\nlyrics.com holds more, but page ${MAX_PAGE} is as far as this tool reads. Narrow the query instead.`;\n}\n\n/** The opening line, which says whether the site matched the title or merely offered rows. */\nfunction headerFor(found: number, loose: boolean, page: number, title: string): string {\n if (found === 0) {\n return `No song on page ${page} matched \"${title}\".`;\n }\n if (loose) {\n return `${found} song(s) lyrics.com offers on page ${page} for \"${title}\":`;\n }\n return `${found} song(s) on page ${page} matching \"${title}\":`;\n}\n\nexport const searchSongsDescription = [\n \"Find songs on lyrics.com by their title.\",\n \"Returns candidate songs with artist, album, year, and the lyrics.com id and URL.\",\n \"Use this when someone names a song and you need its id to call get_lyrics, or to tell covers and versions\",\n \"by different artists apart. An optional 'artist' narrows the list.\",\n \"lyrics.com searches titles and lyrics together, so results are ranked and filtered locally by title match;\",\n 'use match=\"strict\" to keep only genuine title matches.',\n].join(\" \");\n\nexport const searchSongsInput = strictInput({\n title: z.string().min(1).max(120).describe(\"Song title, or part of it.\"),\n artist: z\n .string()\n .max(120)\n .optional()\n .describe(\"Optional artist name, matched case-insensitively against each result.\"),\n limit: z.number().int().min(1).max(50).default(10),\n page: z.number().int().min(1).max(20).default(1),\n match: z\n .enum([\"loose\", \"strict\"])\n .default(\"loose\")\n .describe(\n \"'strict' keeps only results whose title matches the query as a whole title, ignoring suffixes such as \" +\n \"'(Remastered)'. 'loose' keeps anything lyrics.com returned, best match first.\",\n ),\n});\n\nexport const searchSongsOutputShape = {\n title: z.string(),\n artist_filter: z.string().nullable(),\n page: z.number().int(),\n results: z.array(songResultSchema),\n raw_result_count: z.number().int(),\n filtered_out: z.number().int(),\n has_more: z.boolean(),\n next_page: z.number().int().nullable(),\n source: z.literal(\"lyrics.com\"),\n notes: z.array(z.string()),\n};\n\nexport interface SearchSongsArgs {\n title: string;\n artist?: string;\n limit: number;\n page: number;\n match: \"loose\" | \"strict\";\n}\n\n/** Normalized form used for title comparison: no edition suffix, no punctuation noise. */\nfunction normalizeTitle(title: string): string {\n return title\n .replace(/\\s*[[(].*?[\\])]/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim()\n .toLowerCase();\n}\n\n/** 3 = same title, 2 = starts with the query, 1 = contains it, 0 = no title match. */\nexport function titleScore(candidate: string, query: string): number {\n const a = normalizeTitle(candidate);\n const b = normalizeTitle(query);\n if (!(a && b)) {\n return 0;\n }\n if (a === b) {\n return 3;\n }\n if (a.startsWith(b)) {\n return 2;\n }\n if (a.includes(b)) {\n return 1;\n }\n return 0;\n}\n\nexport async function runSearchSongs(\n client: LyricsComClient,\n args: SearchSongsArgs,\n): Promise<ToolResult> {\n try {\n const { data, cached } = await client.search(args.title, args.page);\n const notes: string[] = [];\n if (cached) {\n notes.push(\"Served from this server's short-lived in-memory cache.\");\n }\n\n const artistFilter = args.artist?.trim().toLowerCase() ?? null;\n const unique = dedupeSongs(data.results);\n\n const scored: Array<{ song: SongResult; score: number }> = [];\n let filteredOut = 0;\n\n for (const song of unique) {\n const score = titleScore(song.title, args.title);\n const artistOk = !artistFilter || song.artist.toLowerCase().includes(artistFilter);\n if (!artistOk || (args.match === \"strict\" && score < 2)) {\n filteredOut += 1;\n continue;\n }\n scored.push({ song, score });\n }\n\n // Stable sort by score, so equally scored rows keep lyrics.com's own order.\n scored.sort((a, b) => b.score - a.score);\n const results = scored.slice(0, args.limit).map((entry) => toSongResultOut(entry.song, null));\n\n if (results.length === 0 && data.rawCount > 0) {\n notes.push(\n args.match === \"strict\"\n ? 'lyrics.com returned results but none matched the title strictly. Try match=\"loose\".'\n : \"lyrics.com returned results but none matched the requested artist.\",\n );\n }\n\n const structured = {\n title: args.title,\n artist_filter: args.artist ?? null,\n page: args.page,\n results,\n raw_result_count: data.rawCount,\n filtered_out: filteredOut,\n has_more: data.hasMore,\n next_page: data.hasMore && args.page < MAX_PAGE ? args.page + 1 : null,\n source: \"lyrics.com\" as const,\n notes,\n };\n\n // In loose mode the site returns its own guesses, and a row whose title is\n // not the one asked for is among them. Calling the list a set of matches is\n // how one song's words end up quoted for another.\n const loose = args.match !== \"strict\";\n const header = headerFor(results.length, loose, args.page, args.title);\n // The footer may only name a page the schema would accept, or it sends a\n // caller into an argument that is refused.\n const footer = whatFollowsThisPage(data.hasMore, args.page);\n\n if (results.length > 0 && loose) {\n notes.push(\n `These are lyrics.com's own suggestions for \"${args.title}\", not rows whose title matches it. Check each title before quoting one, or ask again with match=\"strict\".`,\n );\n }\n\n return ok(structured, `${header}\\n${renderResultList(results)}${footer}`, notes);\n } catch (error) {\n return toToolError(error);\n }\n}\n"],"mappings":"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","names":["process","z","process","process","load","load","z","z","z","z","structured","z","z","z","whatFollowsThisPage","z","z","process"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/version.ts","../src/server.ts","../src/lyricscom/cache.ts","../src/errors.ts","../src/lyricscom/parsers/detectThrottle.ts","../src/lyricscom/rateLimiter.ts","../src/lyricscom/http.ts","../src/lyricscom/parsers/searchResults.ts","../src/text/normalize.ts","../src/lyricscom/urls.ts","../src/lyricscom/parsers/selectors.ts","../src/lyricscom/parsers/songPage.ts","../src/lyricscom/client.ts","../src/tools/getLyrics.ts","../src/text/keywordIndex.ts","../src/text/excerpt.ts","../src/tools/arguments.ts","../src/tools/shared.ts","../src/tools/searchLyrics.ts","../src/text/dedupe.ts","../src/tools/searchSongs.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Entry point: an MCP server for lyrics.com over stdio.\n */\n\nimport process from \"node:process\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { createServer } from \"./server.js\";\n\nasync function main(): Promise<void> {\n const config = loadConfig();\n const logger = createLogger(config.logLevel);\n const server = createServer({ config, logger });\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n const shutdown = () => {\n // The close is attempted, and a close that fails is no reason to stay\n // alive: the process leaves either way.\n server\n .close()\n .catch(() => undefined)\n .finally(() => process.exit(0));\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((error: unknown) => {\n process.stderr.write(\n `[mcp-lyricscom] fatal: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`,\n );\n process.exit(1);\n});\n","/**\n * Runtime configuration, read from environment variables.\n *\n * A bad value never crashes the process: an MCP server that dies at startup\n * because of a typo in a client config file is very hard to diagnose from the\n * host application, so invalid input is clamped and reported on stderr.\n */\n\nimport process from \"node:process\";\nimport { PKG_VERSION, REPO_URL } from \"./version.js\";\n\nexport type LogLevel = \"silent\" | \"error\" | \"info\" | \"debug\";\n\nexport interface Config {\n userAgent: string;\n minIntervalMs: number;\n timeoutMs: number;\n maxRetries: number;\n cacheTtlMs: number;\n cacheMaxEntries: number;\n logLevel: LogLevel;\n}\n\n/**\n * Identifies the client honestly. lyrics.com serves this fine; it blocks\n * generic tool agents such as `curl/x.y.z`, which is what the override is for.\n */\nexport const DEFAULT_USER_AGENT = `mcp-lyricscom/${PKG_VERSION} (+${REPO_URL})`;\n\nexport const DEFAULTS = {\n minIntervalMs: 1100,\n timeoutMs: 15_000,\n maxRetries: 3,\n cacheTtlMs: 15 * 60 * 1000,\n cacheMaxEntries: 200,\n logLevel: \"error\" as LogLevel,\n};\n\n/**\n * Floor on the request interval, enforced regardless of configuration.\n *\n * The pacing is what keeps this client from behaving like a crawler against a\n * site that has no API and no published rate limit. Leaving it configurable down\n * to zero would make the politeness of every installation depend on whoever\n * edited a JSON file, so a value below this floor is refused and the default is\n * used instead.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 500;\n\nconst LOG_LEVELS: LogLevel[] = [\"silent\", \"error\", \"info\", \"debug\"];\n\ninterface NumericRange {\n min: number;\n max: number;\n fallback: number;\n}\n\nfunction readNumber(name: string, env: NodeJS.ProcessEnv, range: NumericRange): number {\n const raw = env[name];\n if (raw === undefined || raw.trim() === \"\") {\n return range.fallback;\n }\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`${name}=\"${raw}\" is not a number, using ${range.fallback}`);\n return range.fallback;\n }\n const clamped = Math.min(range.max, Math.max(range.min, Math.round(parsed)));\n if (clamped !== Math.round(parsed)) {\n warn(`${name}=${raw} is out of range, clamped to ${clamped}`);\n }\n return clamped;\n}\n\nfunction warn(message: string): void {\n process.stderr.write(`[mcp-lyricscom] ${message}\\n`);\n}\n\n/**\n * Read the request interval, refusing anything below the floor.\n *\n * A value under the floor falls back to the default rather than to the floor\n * itself: someone who set 0 was not asking for 500, they were asking for no\n * pacing at all, and the safe reading of that request is to ignore it.\n */\nfunction readInterval(env: NodeJS.ProcessEnv): number {\n const raw = env.LYRICSCOM_MIN_INTERVAL_MS;\n if (raw === undefined || raw.trim() === \"\") {\n return DEFAULTS.minIntervalMs;\n }\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`LYRICSCOM_MIN_INTERVAL_MS=\"${raw}\" is not a number, using ${DEFAULTS.minIntervalMs}ms`);\n return DEFAULTS.minIntervalMs;\n }\n\n const rounded = Math.round(parsed);\n if (rounded < MIN_ALLOWED_INTERVAL_MS) {\n warn(\n `LYRICSCOM_MIN_INTERVAL_MS=${raw} is below the ${MIN_ALLOWED_INTERVAL_MS}ms floor and was ignored; ` +\n `using ${DEFAULTS.minIntervalMs}ms. This floor keeps the client from hammering lyrics.com.`,\n );\n return DEFAULTS.minIntervalMs;\n }\n\n return Math.min(60_000, rounded);\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const rawUserAgent = env.LYRICSCOM_USER_AGENT?.trim();\n const rawLogLevel = env.LYRICSCOM_LOG_LEVEL?.trim().toLowerCase();\n\n let logLevel = DEFAULTS.logLevel;\n if (rawLogLevel) {\n if (LOG_LEVELS.includes(rawLogLevel as LogLevel)) {\n logLevel = rawLogLevel as LogLevel;\n } else {\n warn(`LYRICSCOM_LOG_LEVEL=\"${rawLogLevel}\" is unknown, using \"${DEFAULTS.logLevel}\"`);\n }\n }\n\n return {\n userAgent: rawUserAgent || DEFAULT_USER_AGENT,\n minIntervalMs: readInterval(env),\n timeoutMs: readNumber(\"LYRICSCOM_TIMEOUT_MS\", env, {\n min: 1000,\n max: 120_000,\n fallback: DEFAULTS.timeoutMs,\n }),\n maxRetries: readNumber(\"LYRICSCOM_MAX_RETRIES\", env, {\n min: 0,\n max: 10,\n fallback: DEFAULTS.maxRetries,\n }),\n cacheTtlMs: readNumber(\"LYRICSCOM_CACHE_TTL_MS\", env, {\n min: 0,\n max: 24 * 60 * 60 * 1000,\n fallback: DEFAULTS.cacheTtlMs,\n }),\n cacheMaxEntries: readNumber(\"LYRICSCOM_CACHE_MAX_ENTRIES\", env, {\n min: 0,\n max: 10_000,\n fallback: DEFAULTS.cacheMaxEntries,\n }),\n logLevel,\n };\n}\n\nconst LEVEL_RANK: Record<LogLevel, number> = { silent: 0, error: 1, info: 2, debug: 3 };\n\n/**\n * Logs go to stderr without exception. On a stdio transport, stdout carries the\n * protocol and any stray write there corrupts the session.\n */\nexport function createLogger(level: LogLevel) {\n const emit = (at: LogLevel, message: string) => {\n if (LEVEL_RANK[level] >= LEVEL_RANK[at]) {\n process.stderr.write(`[mcp-lyricscom] ${message}\\n`);\n }\n };\n return {\n error: (message: string) => emit(\"error\", message),\n info: (message: string) => emit(\"info\", message),\n debug: (message: string) => emit(\"debug\", message),\n };\n}\n\nexport type Logger = ReturnType<typeof createLogger>;\n","/**\n * Kept in sync with package.json. The User-Agent embeds it, so it must not\n * drift; the release checklist bumps both together.\n */\nexport const PKG_VERSION = \"2.1.0\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-lyricscom\";\n","/**\n * MCP server wiring.\n *\n * One client, one rate limiter and one cache are shared by all three tools:\n * per-tool instances would let three tools each open their own request stream\n * and defeat the throttling protection.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { Config, Logger } from \"./config.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { LyricsComClient } from \"./lyricscom/client.js\";\nimport {\n getLyricsDescription,\n getLyricsInput,\n getLyricsOutputShape,\n runGetLyrics,\n} from \"./tools/getLyrics.js\";\nimport type { GetLyricsArgs } from \"./tools/getLyrics.js\";\nimport {\n runSearchLyrics,\n searchLyricsDescription,\n searchLyricsInput,\n searchLyricsOutputShape,\n} from \"./tools/searchLyrics.js\";\nimport type { SearchLyricsArgs } from \"./tools/searchLyrics.js\";\nimport {\n runSearchSongs,\n searchSongsDescription,\n searchSongsInput,\n searchSongsOutputShape,\n} from \"./tools/searchSongs.js\";\nimport type { SearchSongsArgs } from \"./tools/searchSongs.js\";\nimport { PKG_VERSION } from \"./version.js\";\n\nexport interface CreateServerOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\nconst READ_ONLY = {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n} as const;\n\nexport function createServer(options: CreateServerOptions = {}): McpServer {\n const config = options.config ?? loadConfig();\n const logger = options.logger ?? createLogger(config.logLevel);\n const client = new LyricsComClient({\n config,\n logger,\n ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),\n });\n\n const server = new McpServer(\n { name: \"mcp-lyricscom\", version: PKG_VERSION },\n {\n instructions:\n \"Tools for looking up song lyrics on lyrics.com. No API key is needed. \" +\n \"Typical flow: search_lyrics (by a word in the lyrics) or search_songs (by title) to get a song id, \" +\n \"then get_lyrics with that id. When you show lyrics to a user, credit the artist and link the source URL. \" +\n \"A 'throttled' error means lyrics.com is rate limiting this client, not that the song does not exist.\",\n },\n );\n\n server.registerTool(\n \"search_lyrics\",\n {\n title: \"Search lyrics by word\",\n description: searchLyricsDescription,\n inputSchema: searchLyricsInput,\n outputSchema: z.object(searchLyricsOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runSearchLyrics(client, args as SearchLyricsArgs),\n );\n\n server.registerTool(\n \"search_songs\",\n {\n title: \"Search songs by title\",\n description: searchSongsDescription,\n inputSchema: searchSongsInput,\n outputSchema: z.object(searchSongsOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runSearchSongs(client, args as SearchSongsArgs),\n );\n\n server.registerTool(\n \"get_lyrics\",\n {\n title: \"Get full lyrics\",\n description: getLyricsDescription,\n inputSchema: getLyricsInput,\n outputSchema: z.object(getLyricsOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runGetLyrics(client, args as GetLyricsArgs),\n );\n\n logger.info(\n `ready: user-agent=\"${config.userAgent}\", min interval ${config.minIntervalMs}ms, cache ${config.cacheTtlMs}ms`,\n );\n\n return server;\n}\n","/**\n * In-memory TTL + LRU cache.\n *\n * Nothing is written to disk. The cache exists so that repeated questions about\n * the same song inside one conversation do not hit lyrics.com again, not to\n * build a local copy of the site.\n *\n * A Map iterates in insertion order, so re-inserting on every hit is enough to\n * make the first key the least recently used one.\n */\n\ninterface Entry<V> {\n value: V;\n expiresAt: number;\n}\n\nexport class TtlLruCache<V> {\n private readonly store = new Map<string, Entry<V>>();\n\n constructor(\n private readonly maxEntries: number,\n private readonly ttlMs: number,\n ) {}\n\n get(key: string): V | undefined {\n const entry = this.store.get(key);\n if (!entry) {\n return undefined;\n }\n if (Date.now() >= entry.expiresAt) {\n this.store.delete(key);\n return undefined;\n }\n this.store.delete(key);\n this.store.set(key, entry);\n return entry.value;\n }\n\n set(key: string, value: V): void {\n if (this.maxEntries <= 0 || this.ttlMs <= 0) {\n return;\n }\n this.store.delete(key);\n this.store.set(key, { value, expiresAt: Date.now() + this.ttlMs });\n while (this.store.size > this.maxEntries) {\n const oldest = this.store.keys().next();\n if (oldest.done) {\n break;\n }\n this.store.delete(oldest.value);\n }\n }\n\n get size(): number {\n return this.store.size;\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","/**\n * Error taxonomy surfaced to the calling model.\n *\n * The point of this file is that an LLM must be able to tell \"lyrics.com pushed\n * back on me\" apart from \"there is no such song\". The reference implementation\n * this server replaces collapsed every failure into an empty list, which made\n * rate limiting look like an absence of results.\n */\n\nexport type ErrorCode =\n | \"throttled\"\n | \"blocked_user_agent\"\n | \"not_found\"\n | \"invalid_input\"\n | \"parse_failure\"\n | \"network_error\"\n | \"timeout\";\n\nexport interface ErrorDetails {\n url?: string;\n status?: number;\n retryAfterMs?: number;\n hint?: string;\n}\n\nexport class LyricsComError extends Error {\n constructor(\n readonly code: ErrorCode,\n message: string,\n readonly details: ErrorDetails = {},\n ) {\n super(message);\n this.name = \"LyricsComError\";\n }\n}\n\nconst ISSUES_URL = \"https://github.com/smeet666/mcp-lyricscom/issues\";\n\nexport function throttled(url: string, retryAfterMs: number): LyricsComError {\n return new LyricsComError(\n \"throttled\",\n \"lyrics.com is rate limiting this client. It answered with an empty response instead of a page. \" +\n \"This does NOT mean there are no results for this query.\",\n {\n url,\n retryAfterMs,\n hint:\n \"Wait at least a minute before calling the same tool again with the same arguments. Once lyrics.com \" +\n \"starts throttling, the window commonly lasts several minutes, so retrying immediately will fail again. \" +\n \"If it keeps happening, raise LYRICSCOM_MIN_INTERVAL_MS in your MCP client configuration.\",\n },\n );\n}\n\nexport function blockedUserAgent(url: string): LyricsComError {\n return new LyricsComError(\n \"blocked_user_agent\",\n \"lyrics.com refused the request because of the User-Agent this server sends (HTTP 403).\",\n {\n url,\n status: 403,\n hint:\n \"Set the LYRICSCOM_USER_AGENT environment variable in your MCP client configuration to a different \" +\n \"value. See the Configuration section of the project README.\",\n },\n );\n}\n\nexport function notFound(url: string): LyricsComError {\n return new LyricsComError(\"not_found\", \"lyrics.com has no page at this address.\", {\n url,\n status: 404,\n hint: \"Check the song id. Ids come from the search tools; they are not stable to guess by hand.\",\n });\n}\n\nexport function parseFailure(url: string, what: string): LyricsComError {\n return new LyricsComError(\n \"parse_failure\",\n `The page loaded but the expected lyrics.com markup was not found (${what}). The site layout may have changed.`,\n { url, hint: `Please report this, with the query you used, at ${ISSUES_URL}` },\n );\n}\n\nexport function invalidInput(message: string, hint?: string): LyricsComError {\n return new LyricsComError(\"invalid_input\", message, hint ? { hint } : {});\n}\n","/**\n * Response classification.\n *\n * lyrics.com does not answer rate limiting with 429. It answers with HTTP 202\n * and a zero-byte body, which a naive client reads as a successful but empty\n * page. Every response therefore goes through this classifier before any\n * parsing happens.\n */\n\nconst CLOSING_HTML_TAG = /<\\/html>/i;\n\nexport type ResponseVerdict =\n | { kind: \"ok\"; html: string }\n | { kind: \"throttled\"; reason: ThrottleReason }\n | { kind: \"blocked\"; status: number }\n | { kind: \"not-found\" }\n | { kind: \"server-error\"; status: number };\n\nexport type ThrottleReason =\n | \"status-202\"\n | \"status-429\"\n | \"status-503\"\n | \"empty-body\"\n | \"short-body\";\n\n/**\n * A real search page weighs about 187 KB and a song page 60 KB or more, so a\n * body this small is never a genuine lyrics.com page.\n */\nexport const MIN_PLAUSIBLE_HTML = 2000;\n\nexport function classifyResponse(status: number, body: string): ResponseVerdict {\n if (status === 202) {\n return { kind: \"throttled\", reason: \"status-202\" };\n }\n if (status === 429) {\n return { kind: \"throttled\", reason: \"status-429\" };\n }\n if (status === 503) {\n return { kind: \"throttled\", reason: \"status-503\" };\n }\n if (status === 403) {\n return { kind: \"blocked\", status };\n }\n if (status === 404) {\n return { kind: \"not-found\" };\n }\n if (status >= 500) {\n return { kind: \"server-error\", status };\n }\n\n const trimmed = body.trim();\n if (trimmed.length === 0) {\n return { kind: \"throttled\", reason: \"empty-body\" };\n }\n if (trimmed.length < MIN_PLAUSIBLE_HTML && !CLOSING_HTML_TAG.test(trimmed)) {\n return { kind: \"throttled\", reason: \"short-body\" };\n }\n\n return { kind: \"ok\", html: body };\n}\n","/**\n * Serial request queue with an adaptive minimum interval.\n *\n * lyrics.com tolerates a steady trickle but stops answering after a short burst,\n * so requests are run one at a time with a floor on the gap between starts.\n * When the site does push back, the interval doubles and then decays back down\n * as requests succeed, which recovers faster than a fixed delay and behaves\n * better than hammering at a constant rate.\n */\n\nexport interface RateLimiterOptions {\n minIntervalMs: number;\n maxIntervalMs?: number;\n}\n\nconst DEFAULT_MAX_INTERVAL_MS = 10_000;\n\nexport class RateLimiter {\n private readonly baseIntervalMs: number;\n private readonly maxIntervalMs: number;\n private intervalMs: number;\n /** Tail of the queue: each task chains onto the previous one. */\n private tail: Promise<unknown> = Promise.resolve();\n private lastStart = 0;\n\n constructor(options: RateLimiterOptions) {\n this.baseIntervalMs = Math.max(0, options.minIntervalMs);\n this.maxIntervalMs = Math.max(\n this.baseIntervalMs,\n options.maxIntervalMs ?? DEFAULT_MAX_INTERVAL_MS,\n );\n this.intervalMs = this.baseIntervalMs;\n }\n\n get currentIntervalMs(): number {\n return this.intervalMs;\n }\n\n /** Queue a task. Tasks run in call order, one at a time. */\n schedule<T>(task: () => Promise<T>): Promise<T> {\n const run = this.tail.then(async () => task());\n // The queue must keep draining even when a task rejects.\n this.tail = run.then(\n () => undefined,\n () => undefined,\n );\n return run;\n }\n\n /**\n * Wait for this request's slot, then claim it.\n *\n * Called once per upstream request rather than once per task, because a task\n * runs a whole retry chain: stamping only its start would let the next task\n * follow the chain's last request with no gap at all.\n */\n async beforeRequest(): Promise<void> {\n await this.waitForSlot();\n this.lastStart = Date.now();\n }\n\n /** Called after the site signals it is under load. */\n penalize(): void {\n const next = this.intervalMs === 0 ? 250 : this.intervalMs * 2;\n this.intervalMs = Math.min(this.maxIntervalMs, next);\n }\n\n /** Called after a success, so a single hiccup does not slow things down forever. */\n relax(): void {\n this.intervalMs = Math.max(this.baseIntervalMs, Math.floor(this.intervalMs * 0.75));\n }\n\n private async waitForSlot(): Promise<void> {\n if (this.intervalMs === 0 || this.lastStart === 0) {\n return;\n }\n const elapsed = Date.now() - this.lastStart;\n // Clamped to the interval: a clock stepped backwards, by NTP or a resumed\n // virtual machine, would otherwise make this wait for the size of the step,\n // and the queue is serial so every pending request would wait behind it.\n const remaining = Math.min(this.intervalMs, this.intervalMs - elapsed);\n if (remaining > 0) {\n await sleep(remaining);\n }\n }\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * HTTP layer: one GET, classified, with backoff.\n *\n * The retry policy is domain specific rather than generic, because the failure\n * this server exists to handle does not look like a failure at the transport\n * level: lyrics.com answers a throttled request with HTTP 202 and an empty\n * body. Only `classifyResponse` can tell that apart from a real page.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport { blockedUserAgent, LyricsComError, notFound, throttled } from \"../errors.js\";\nimport { classifyResponse } from \"./parsers/detectThrottle.js\";\nimport { type RateLimiter, sleep } from \"./rateLimiter.js\";\n\nconst BACKOFF_BASE_MS = 2000;\nconst BACKOFF_FACTOR = 2;\nconst BACKOFF_MAX_MS = 20_000;\n\n/** Exponential backoff with jitter, so parallel clients do not resynchronise. */\nexport function backoffDelay(attempt: number, random: () => number = Math.random): number {\n const uncapped = BACKOFF_BASE_MS * BACKOFF_FACTOR ** attempt;\n const capped = Math.min(BACKOFF_MAX_MS, uncapped);\n return Math.round(capped * (0.5 + random() * 0.5));\n}\n\nexport interface HttpDeps {\n config: Config;\n limiter: RateLimiter;\n logger: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/**\n * Fetch one page as HTML, retrying transient conditions.\n *\n * The whole retry loop, backoff sleeps included, runs inside a single limiter\n * slot. Waiting outside it would let a queued request slip into the same\n * throttling window that the current one is backing away from.\n */\nexport async function fetchHtml(url: string, deps: HttpDeps): Promise<string> {\n const { config, limiter, logger } = deps;\n const doFetch = deps.fetchImpl ?? fetch;\n\n return await limiter.schedule(async () => {\n let lastError: LyricsComError | undefined;\n\n // Set when the site says how long to stay away; it replaces our own guess\n // for the next attempt. Applied here rather than where it is read, so no\n // wait is ever served after the last attempt, when nobody would use it.\n let askedWaitMs: number | null = null;\n\n for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {\n if (attempt > 0) {\n const delay = Math.min(askedWaitMs ?? backoffDelay(attempt - 1), BACKOFF_MAX_MS);\n askedWaitMs = null;\n logger.info(`retry ${attempt}/${config.maxRetries} in ${delay}ms for ${url}`);\n await sleep(delay);\n }\n\n let status: number;\n let body: string;\n let retryAfterMs: number | null = null;\n try {\n await limiter.beforeRequest();\n const response = await doFetch(url, {\n headers: {\n \"User-Agent\": config.userAgent,\n Accept: \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n \"Accept-Language\": \"en-US,en;q=0.9,fr;q=0.8\",\n },\n redirect: \"follow\",\n signal: AbortSignal.timeout(config.timeoutMs),\n });\n status = response.status;\n retryAfterMs = parseRetryAfter(response.headers.get(\"retry-after\"));\n body = await response.text();\n } catch (error) {\n lastError = asTransportError(error, url);\n logger.debug(`${lastError.code} for ${url}: ${lastError.message}`);\n continue;\n }\n\n const verdict = classifyResponse(status, body);\n switch (verdict.kind) {\n case \"ok\":\n limiter.relax();\n return verdict.html;\n\n case \"throttled\":\n limiter.penalize();\n logger.info(\n `throttled (${verdict.reason}) on ${url}, interval now ${limiter.currentIntervalMs}ms`,\n );\n // A server that says when to come back knows better than our own guess.\n askedWaitMs = retryAfterMs;\n lastError = throttled(url, retryAfterMs ?? backoffDelay(attempt));\n continue;\n\n case \"server-error\":\n lastError = new LyricsComError(\n \"network_error\",\n `lyrics.com returned HTTP ${verdict.status}.`,\n {\n url,\n status: verdict.status,\n },\n );\n continue;\n\n // Retrying these would only repeat the same answer.\n case \"blocked\":\n throw blockedUserAgent(url);\n case \"not-found\":\n throw notFound(url);\n\n // Every kind the classifier declares is handled above, and the linter\n // holds that. This clause is the runtime backstop: a response nothing\n // above matched leaves as an error rather than as a silent retry.\n default:\n throw new LyricsComError(\n \"network_error\",\n `lyrics.com returned a response this client cannot classify for ${url}.`,\n { url },\n );\n }\n }\n\n throw lastError ?? new LyricsComError(\"network_error\", `Could not fetch ${url}.`, { url });\n });\n}\n\n/** `Retry-After` carries either seconds or an HTTP date. */\nfunction parseRetryAfter(raw: string | null): number | null {\n if (!raw) {\n return null;\n }\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) {\n return Math.round(seconds * 1000);\n }\n const when = Date.parse(raw);\n if (Number.isNaN(when)) {\n return null;\n }\n return Math.max(0, when - Date.now());\n}\n\nfunction asTransportError(error: unknown, url: string): LyricsComError {\n if (error instanceof LyricsComError) {\n return error;\n }\n const name = error instanceof Error ? error.name : \"\";\n if (name === \"TimeoutError\" || name === \"AbortError\") {\n return new LyricsComError(\"timeout\", \"lyrics.com did not answer in time.\", {\n url,\n hint: \"Raise LYRICSCOM_TIMEOUT_MS if this happens often on a slow connection.\",\n });\n }\n const message = error instanceof Error ? error.message : String(error);\n return new LyricsComError(\"network_error\", `Could not reach lyrics.com: ${message}`, { url });\n}\n","/**\n * Parser for a lyrics.com search results page.\n *\n * Page 1 (`/lyrics/{term}`) and later pages (`/serp.php?st=...&p=N`) share the\n * same markup, so one parser covers both.\n */\n\nimport process from \"node:process\";\nimport { load } from \"cheerio\";\nimport { parseFailure } from \"../../errors.js\";\nimport type { SearchPage, SongResult } from \"../../types.js\";\nimport { cleanInlineText, cleanLyricsText } from \"../../text/normalize.js\";\nimport { extractSongId, RESULTS_PER_PAGE, toAbsoluteUrl } from \"../urls.js\";\nimport { SEL } from \"./selectors.js\";\n\n/** A year a record can plausibly carry, from the 1800s to this century. */\nconst YEAR = /\\b(1[89]\\d{2}|20\\d{2})\\b/;\n\nexport interface ParseSearchOptions {\n page: number;\n /** Reported in errors so a failure points at the request that caused it. */\n url: string;\n}\n\nexport function parseSearchResults(html: string, options: ParseSearchOptions): SearchPage {\n const $ = load(html);\n const rows = $(SEL.serpItem);\n\n if (rows.length === 0) {\n // An empty result set still renders the page chrome. Its absence means the\n // response was not the page we asked for, which must not be reported as\n // \"no songs found\".\n if ($(SEL.serpShell).length === 0) {\n throw parseFailure(options.url, \"no result rows and no recognizable search page structure\");\n }\n return { results: [], page: options.page, rawCount: 0, hasMore: false };\n }\n\n const results: SongResult[] = [];\n let skipped = 0;\n\n rows.each((_, element) => {\n const row = $(element);\n const titleLink = row.find(SEL.resultTitle).first();\n const href = titleLink.attr(\"href\");\n const title = cleanInlineText(titleLink.text());\n const artist = cleanInlineText(row.find(SEL.resultArtist).first().text());\n\n const id = href ? extractSongId(href) : null;\n if (!(id && title && artist)) {\n skipped += 1;\n return;\n }\n\n const album = cleanInlineText(row.find(SEL.resultAlbum).first().text()) || null;\n const year = parseYear(row.find(SEL.resultYear).first().text());\n const snippetRaw = row.find(SEL.resultSnippet).first().text();\n const snippet = snippetRaw ? cleanLyricsText(snippetRaw) || null : null;\n\n results.push({\n id,\n url: toAbsoluteUrl(href as string),\n title,\n artist,\n album,\n year,\n snippet,\n });\n });\n\n if (results.length === 0) {\n throw parseFailure(options.url, `${rows.length} result rows matched but none could be read`);\n }\n if (skipped > 0) {\n process.stderr.write(\n `[mcp-lyricscom] skipped ${skipped} unreadable result rows on ${options.url}\\n`,\n );\n }\n\n return {\n results,\n page: options.page,\n rawCount: rows.length,\n hasMore: rows.length >= RESULTS_PER_PAGE,\n };\n}\n\n/** Year cells look like \"1971\" but are sometimes decorated or empty. */\nfunction parseYear(raw: string): number | null {\n const match = YEAR.exec(raw);\n if (!match?.[1]) {\n return null;\n }\n return Number.parseInt(match[1], 10);\n}\n","/**\n * Text cleaning for scraped content: HTML entities, mojibake, whitespace.\n */\n\n/** The letters UTF-8 read as Latin-1 leaves behind. */\nconst MOJIBAKE_MARKS = /[ÃÂ]/;\n\nconst HTML_ENTITIES: Record<string, string> = {\n amp: \"&\",\n lt: \"<\",\n gt: \">\",\n quot: '\"',\n apos: \"'\",\n nbsp: \" \",\n hellip: \"…\",\n mdash: \"—\",\n ndash: \"–\",\n rsquo: \"’\",\n lsquo: \"‘\",\n ldquo: \"“\",\n rdquo: \"”\",\n};\n\n/** Decode named and numeric HTML entities. */\nexport function unescapeHtml(text: string): string {\n if (!text) {\n return text;\n }\n return text.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (match, entity: string) => {\n if (entity.startsWith(\"#\")) {\n const isHex = entity[1] === \"x\" || entity[1] === \"X\";\n const codePoint = Number.parseInt(isHex ? entity.slice(2) : entity.slice(1), isHex ? 16 : 10);\n if (Number.isNaN(codePoint) || codePoint < 0 || codePoint > 0x10ffff) {\n return match;\n }\n try {\n return String.fromCodePoint(codePoint);\n } catch {\n return match;\n }\n }\n return HTML_ENTITIES[entity.toLowerCase()] ?? match;\n });\n}\n\n/**\n * Repair UTF-8 bytes that were decoded as Latin-1, which turns \"é\" into \"é\".\n * lyrics.com serves French titles that occasionally arrive in this state.\n * Re-encoding through latin1 recovers the original text; the round trip is only\n * kept when it removes replacement characters, so correct text is never touched.\n */\nexport function fixEncodingIssues(text: string): string {\n if (!(text && MOJIBAKE_MARKS.test(text))) {\n return text;\n }\n try {\n const repaired = Buffer.from(text, \"latin1\").toString(\"utf8\");\n if (repaired.includes(\"�\")) {\n return text;\n }\n return repaired;\n } catch {\n return text;\n }\n}\n\n/** Collapse runs of whitespace and trim. */\nexport function collapseWhitespace(text: string): string {\n return text.replace(/\\s+/g, \" \").trim();\n}\n\n/** Full cleaning pass for a scraped title or artist name. */\nexport function cleanInlineText(text: string): string {\n if (!text) {\n return \"\";\n }\n return collapseWhitespace(fixEncodingIssues(unescapeHtml(text)));\n}\n\n/**\n * Normalize a lyrics block: trim each line, drop leading and trailing blank\n * lines, and collapse runs of blank lines to a single verse separator.\n */\nexport function cleanLyricsText(text: string): string {\n if (!text) {\n return \"\";\n }\n const normalized = fixEncodingIssues(unescapeHtml(text)).replace(/\\r\\n?/g, \"\\n\");\n const lines = normalized.split(\"\\n\").map((line) => line.trim());\n\n const out: string[] = [];\n for (const line of lines) {\n if (line === \"\" && (out.length === 0 || out.at(-1) === \"\")) {\n continue;\n }\n out.push(line);\n }\n while (out.length > 0 && out.at(-1) === \"\") {\n out.pop();\n }\n return out.join(\"\\n\");\n}\n","/**\n * URL construction and validation for lyrics.com.\n */\n\nimport { invalidInput } from \"../errors.js\";\n\nconst ABSOLUTE_URL = /^https?:\\/\\//i;\nconst DIGITS_ONLY = /^\\d+$/;\n\nexport const BASE_URL = \"https://www.lyrics.com\";\n\n/** lyrics.com renders 24 rows per search page. */\nexport const RESULTS_PER_PAGE = 24;\n\n/**\n * Song page paths come in two shapes: the current `/lyric-lf/{id}/...` and the\n * older `/lyric/{id}/...` still present in archived links.\n */\nexport const SONG_HREF_RE = /^\\/lyric(?:-lf)?\\/(\\d+)\\//;\n\nconst ALLOWED_HOSTS = new Set([\"lyrics.com\", \"www.lyrics.com\"]);\n\n/**\n * Percent-encode a search term the way lyrics.com expects, with spaces as `+`.\n */\nfunction encodeTerm(term: string): string {\n return encodeURIComponent(term).replace(/%20/g, \"+\");\n}\n\n/**\n * Page 1 lives at a path, later pages at a query string. Both return the same\n * markup, so the parser does not need to know which one produced it.\n */\nexport function buildSearchUrl(term: string, page: number): string {\n const encoded = encodeTerm(term);\n if (page <= 1) {\n return `${BASE_URL}/lyrics/${encoded}`;\n }\n return `${BASE_URL}/serp.php?st=${encoded}&p=${page}`;\n}\n\nexport function buildSongUrl(id: string): string {\n return `${BASE_URL}/lyric-lf/${id}/`;\n}\n\n/** True only for lyrics.com itself, so a hostile URL cannot be used as a proxy. */\nexport function isLyricsComHost(rawUrl: string): boolean {\n let parsed: URL;\n try {\n parsed = new URL(rawUrl);\n } catch {\n return false;\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n return false;\n }\n return ALLOWED_HOSTS.has(parsed.hostname.toLowerCase());\n}\n\n/** Extract the numeric song id from a lyrics.com song path or absolute URL. */\nexport function extractSongId(hrefOrUrl: string): string | null {\n let path = hrefOrUrl;\n if (ABSOLUTE_URL.test(hrefOrUrl)) {\n if (!isLyricsComHost(hrefOrUrl)) {\n return null;\n }\n path = new URL(hrefOrUrl).pathname;\n }\n if (!path.startsWith(\"/\")) {\n path = `/${path}`;\n }\n const match = SONG_HREF_RE.exec(path);\n return match?.[1] ?? null;\n}\n\n/**\n * Resolve the `id` / `url` pair accepted by get_lyrics into a canonical\n * request URL. `id` wins when both are given.\n */\nexport function resolveSongRef(input: { id?: string; url?: string }): { id: string; url: string } {\n if (input.id) {\n if (!DIGITS_ONLY.test(input.id)) {\n throw invalidInput(\n `\"${input.id}\" is not a lyrics.com song id.`,\n \"Ids are digits only, as returned by the search tools.\",\n );\n }\n return { id: input.id, url: buildSongUrl(input.id) };\n }\n\n if (input.url) {\n if (!isLyricsComHost(input.url)) {\n throw invalidInput(\n \"Only www.lyrics.com URLs are accepted.\",\n \"Pass the source_url returned by search_lyrics or search_songs, or use the numeric id instead.\",\n );\n }\n const id = extractSongId(input.url);\n if (!id) {\n throw invalidInput(\n \"That lyrics.com URL is not a song page.\",\n \"Song pages look like https://www.lyrics.com/lyric-lf/1234567/Artist/Title\",\n );\n }\n return { id, url: input.url };\n }\n\n throw invalidInput(\"Either 'id' or 'url' must be provided.\");\n}\n\n/** Absolute URL for a href scraped out of a result row. */\nexport function toAbsoluteUrl(href: string): string {\n return new URL(href, BASE_URL).toString();\n}\n","/**\n * Every lyrics.com CSS selector lives here, so a site redesign is a one-file fix.\n *\n * Selectors deliberately target a single stable class rather than a full class\n * list (`div.sec-lyric`, not `div.sec-lyric.clearfix`), so that a layout tweak\n * adding or removing a utility class does not break extraction.\n */\n\nexport const SEL = {\n /** One search result row. */\n serpItem: \"div.sec-lyric\",\n /**\n * Page chrome that is present whether or not there are results. Used to tell\n * \"zero results\" apart from \"the markup changed\", which must not look alike.\n */\n serpShell: \"#lyric-search, .lyric-search, .pagination, #main-content, form[action*='serp']\",\n resultTitle: \"p.lyric-meta-title a\",\n /**\n * Rows tied to an album expose the artist under `lyric-meta-album-artist`,\n * rows without one under `lyric-meta-artists`. Both shapes appear on the same\n * page, so missing the second silently drops results.\n */\n resultArtist: \"p.lyric-meta-album-artist a, p.lyric-meta-artists a\",\n resultAlbum: \"p.lyric-meta-album\",\n resultYear: \"p.lyric-meta-album-year\",\n resultSnippet: \"pre.lyric-body\",\n\n /** Full lyrics on a song page, with the historical fallback. */\n songBody: \"pre#lyric-body-text, pre.lyric-body\",\n /** Marker for a valid song page that simply has no lyrics on file. */\n songNoData: \"div.lyric-no-data\",\n songTitle: \"#lyric-title-text, .lyric-title\",\n songArtist: \".lyric-artist a, .lyric-artist\",\n} as const;\n","/**\n * Parser for a lyrics.com song page.\n */\n\nimport { load } from \"cheerio\";\nimport type { CheerioAPI } from \"cheerio\";\nimport { parseFailure } from \"../../errors.js\";\nimport type { SongPage } from \"../../types.js\";\nimport { cleanInlineText, cleanLyricsText } from \"../../text/normalize.js\";\nimport { SEL } from \"./selectors.js\";\n\n/** The wording the site puts in the title of a page holding no words. */\nconst NO_LYRICS_FOUND = /no lyrics found/i;\n\nexport interface ParseSongOptions {\n id: string;\n url: string;\n}\n\nexport function parseSongPage(html: string, options: ParseSongOptions): SongPage {\n const $ = load(html);\n\n const title = cleanInlineText($(SEL.songTitle).first().text()) || null;\n const artist = cleanInlineText($(SEL.songArtist).first().text()) || null;\n\n const body = $(SEL.songBody).first();\n if (body.length === 0) {\n // A valid song page can legitimately carry no lyrics. That is an answer,\n // not a failure, so it is only treated as breakage when the page says\n // nothing about why the text is missing.\n const declaresNoLyrics =\n $(SEL.songNoData).length > 0 || NO_LYRICS_FOUND.test($(\"title\").text());\n if (!declaresNoLyrics) {\n throw parseFailure(options.url, \"no lyrics container and no 'no lyrics' marker\");\n }\n return { id: options.id, url: options.url, title, artist, lyrics: \"\", hasLyrics: false };\n }\n\n const lyrics = cleanLyricsText(extractBodyText($, body));\n if (!lyrics) {\n return { id: options.id, url: options.url, title, artist, lyrics: \"\", hasLyrics: false };\n }\n\n return { id: options.id, url: options.url, title, artist, lyrics, hasLyrics: true };\n}\n\n/**\n * lyrics.com wraps many individual words in links to its dictionary pages.\n * Those anchors are unwrapped in the raw HTML rather than through the DOM,\n * because DOM-level removal loses the line breaks that give lyrics their shape.\n */\nfunction extractBodyText($: CheerioAPI, body: ReturnType<CheerioAPI>): string {\n const rawHtml = $.html(body);\n const withoutAnchors = rawHtml.replace(/<a[^>]*>([\\s\\S]*?)<\\/a>/gi, \"$1\");\n const withNewlines = withoutAnchors.replace(/<br\\s*\\/?>/gi, \"\\n\");\n return load(withNewlines).root().text();\n}\n","/**\n * High-level lyrics.com client.\n *\n * This module knows nothing about MCP, which keeps it unit-testable against\n * plain strings and usable as a plain library through the `./client` export.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport {\n DEFAULT_USER_AGENT,\n MIN_ALLOWED_INTERVAL_MS,\n createLogger,\n loadConfig,\n} from \"../config.js\";\nimport type { SearchPage, SongPage } from \"../types.js\";\nimport { TtlLruCache } from \"./cache.js\";\nimport { fetchHtml } from \"./http.js\";\nimport { parseSearchResults } from \"./parsers/searchResults.js\";\nimport { parseSongPage } from \"./parsers/songPage.js\";\nimport { RateLimiter } from \"./rateLimiter.js\";\nimport { buildSearchUrl, resolveSongRef } from \"./urls.js\";\n\n/** The names a User-Agent carries when it passes traffic off as a browser. */\nconst BROWSER_IDENTITY = /mozilla\\/|applewebkit|chrome\\/|safari\\/|gecko/i;\n\nexport interface LyricsComClientOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\nexport interface FetchOutcome<T> {\n data: T;\n /** True when the page came from the in-memory cache rather than the network. */\n cached: boolean;\n}\n\n/**\n * Apply the guarantees this project makes about its own traffic.\n *\n * The environment parser already enforces both, but `LyricsComClient` is published as a\n * library through the `./client` export and takes a caller-built config, so\n * without this the pacing floor and the honest identity are optional for anyone\n * importing it. lyrics.com answers a burst by refusing, and an honest User-Agent is what it accepts, and those promises hold on every path.\n *\n * A caller may still name their own application in the User-Agent. Passing the\n * traffic off as a browser is a different thing, and gets the project's own\n * identity appended so it stays attributable.\n */\nfunction withGuarantees(config: Config): Config {\n const userAgent = BROWSER_IDENTITY.test(config.userAgent)\n ? `${config.userAgent} ${DEFAULT_USER_AGENT}`\n : config.userAgent;\n return {\n ...config,\n userAgent,\n minIntervalMs: Math.max(MIN_ALLOWED_INTERVAL_MS, config.minIntervalMs),\n };\n}\n\nexport class LyricsComClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly limiter: RateLimiter;\n private readonly cache: TtlLruCache<unknown>;\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: LyricsComClientOptions = {}) {\n this.config = withGuarantees(options.config ?? loadConfig());\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.limiter = new RateLimiter({ minIntervalMs: this.config.minIntervalMs });\n this.cache = new TtlLruCache<unknown>(this.config.cacheMaxEntries, this.config.cacheTtlMs);\n this.fetchImpl = options.fetchImpl;\n }\n\n async search(term: string, page: number): Promise<FetchOutcome<SearchPage>> {\n const url = buildSearchUrl(term, page);\n return await this.fetchParsed(url, (html) => parseSearchResults(html, { page, url }));\n }\n\n async getSong(ref: { id?: string; url?: string }): Promise<FetchOutcome<SongPage>> {\n const { id, url } = resolveSongRef(ref);\n return await this.fetchParsed(url, (html) => parseSongPage(html, { id, url }));\n }\n\n /**\n * Fetch, parse, then cache. In that order: a page that could not be read is\n * never stored, so a bad minute at lyrics.com cannot be replayed from memory for\n * the rest of the cache lifetime, leaving the tool unable to recover after\n * the site comes back.\n *\n * The cached value is the parsed result rather than the raw page, which also\n * keeps a few hundred kilobytes of markup per entry out of memory.\n */\n private async fetchParsed<T>(url: string, parse: (html: string) => T): Promise<FetchOutcome<T>> {\n const hit = this.cache.get(url);\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${url}`);\n return { data: hit as T, cached: true };\n }\n\n const html = await fetchHtml(url, {\n config: this.config,\n limiter: this.limiter,\n logger: this.logger,\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n });\n\n const data = parse(html);\n this.cache.set(url, data);\n return { data, cached: false };\n }\n}\n","/**\n * get_lyrics: fetch the full lyrics of one song.\n */\n\nimport { z } from \"zod\";\nimport type { LyricsComClient } from \"../lyricscom/client.js\";\nimport { findMatchingLine } from \"../text/excerpt.js\";\nimport { strictInput } from \"./arguments.js\";\nimport { ok, toToolError, truncate } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\n/** Where the answer stopped in the words, and what to ask for to go on. */\nfunction howFarTheTextGoes(pastTheEnd: boolean, offset: number, nextOffset: number | null): string {\n if (pastTheEnd) {\n return `Nothing at offset=${offset}: that is past the end. Call again with offset=0.`;\n }\n if (nextOffset === null) {\n return \"Complete.\";\n }\n return `Truncated: call again with offset=${nextOffset} to continue.`;\n}\n\n/** What was found of the word a caller asked to be shown, when they asked for one. */\nfunction whereTheWordIs(\n highlight: { word: string; found: boolean; line_number: number | null } | null | undefined,\n): string {\n if (!highlight) {\n return \"\";\n }\n if (!highlight.found) {\n return `\"${highlight.word}\" does not appear in these lyrics.`;\n }\n return `\"${highlight.word}\" appears on line ${highlight.line_number}.`;\n}\n\nexport const getLyricsDescription = [\n \"Fetch the full lyrics of one song from lyrics.com, given the song id or URL returned by search_lyrics or search_songs.\",\n \"Lyrics can be long, so the response is truncated by default: check 'truncated' and call again with 'offset' set to\",\n \"'next_offset' to continue reading.\",\n 'Some lyrics.com pages legitimately have no lyrics on file; those come back with status \"no_lyrics\" and an empty',\n \"lyrics field, which is a valid answer and not an error, so do not retry them.\",\n \"Always cite 'source_url' and the artist when showing lyrics to a user.\",\n].join(\" \");\n\nexport const getLyricsInput = strictInput({\n id: z\n .string()\n .regex(/^\\d+$/, \"Song ids are digits only.\")\n .optional()\n .describe(\"lyrics.com numeric song id, as returned by the search tools. Preferred over 'url'.\"),\n url: z\n .string()\n .url()\n .optional()\n .describe(\n \"Full lyrics.com song URL. Only www.lyrics.com URLs are accepted. Ignored when 'id' is given.\",\n ),\n max_chars: z\n .number()\n .int()\n .min(200)\n .max(20_000)\n .default(6000)\n .describe(\"Maximum characters of lyrics text to return in this call.\"),\n offset: z\n .number()\n .int()\n .min(0)\n .default(0)\n .describe(\"Character offset to resume from, for lyrics longer than max_chars.\"),\n highlight: z\n .string()\n .max(120)\n .optional()\n .describe(\n \"Optional word. When set, the response reports whether it appears in the lyrics and on which line.\",\n ),\n});\n\nexport const getLyricsOutputShape = {\n status: z.enum([\"ok\", \"no_lyrics\"]),\n id: z.string(),\n title: z.string().nullable(),\n artist: z.string().nullable(),\n source_url: z.string(),\n lyrics: z.string().describe(\"Slice of the lyrics text. Empty when status is 'no_lyrics'.\"),\n total_chars: z.number().int(),\n returned_chars: z.number().int(),\n offset: z.number().int(),\n next_offset: z.number().int().nullable(),\n truncated: z.boolean(),\n line_count: z.number().int(),\n highlight: z\n .object({\n word: z.string(),\n found: z.boolean(),\n line_number: z.number().int().nullable(),\n line: z.string().nullable(),\n })\n .nullable(),\n attribution: z.string().describe(\"Ready-to-display credit line.\"),\n source: z.literal(\"lyrics.com\"),\n notes: z.array(z.string()),\n};\n\nexport interface GetLyricsArgs {\n id?: string;\n url?: string;\n max_chars: number;\n offset: number;\n highlight?: string;\n}\n\n/**\n * Cut at the last newline that fits, so a continuation never splits a line in\n * half. Falls back to a hard cut for a single line longer than the budget.\n */\nexport function sliceAtLineBoundary(\n text: string,\n offset: number,\n maxChars: number,\n): { slice: string; nextOffset: number | null } {\n if (offset >= text.length) {\n return { slice: \"\", nextOffset: null };\n }\n\n const remaining = text.slice(offset);\n if (remaining.length <= maxChars) {\n return { slice: remaining, nextOffset: null };\n }\n\n const window = remaining.slice(0, maxChars);\n const lastBreak = window.lastIndexOf(\"\\n\");\n const cut = lastBreak > 0 ? lastBreak : maxChars;\n return { slice: remaining.slice(0, cut), nextOffset: offset + cut };\n}\n\nexport async function runGetLyrics(\n client: LyricsComClient,\n args: GetLyricsArgs,\n): Promise<ToolResult> {\n try {\n const ref: { id?: string; url?: string } = {};\n if (args.id) {\n ref.id = args.id;\n } else if (args.url) {\n ref.url = args.url;\n }\n\n const { data, cached } = await client.getSong(ref);\n const notes: string[] = [];\n if (cached) {\n notes.push(\"Served from this server's short-lived in-memory cache.\");\n }\n const credit = [data.title, data.artist].filter(Boolean).join(\" · \");\n const attribution = `${credit || \"Lyrics\"} via lyrics.com (${data.url})`;\n\n if (!data.hasLyrics) {\n const structured = {\n status: \"no_lyrics\" as const,\n id: data.id,\n title: data.title,\n artist: data.artist,\n source_url: data.url,\n lyrics: \"\",\n total_chars: 0,\n returned_chars: 0,\n offset: 0,\n next_offset: null,\n truncated: false,\n notes,\n line_count: 0,\n highlight: null,\n attribution,\n source: \"lyrics.com\" as const,\n };\n return ok(\n structured,\n `lyrics.com has a page for this song but no lyrics on file.\\n${attribution}`,\n );\n }\n\n const { slice, nextOffset } = sliceAtLineBoundary(data.lyrics, args.offset, args.max_chars);\n const match = args.highlight ? findMatchingLine(data.lyrics, args.highlight) : null;\n\n const structured = {\n status: \"ok\" as const,\n id: data.id,\n title: data.title,\n artist: data.artist,\n source_url: data.url,\n lyrics: slice,\n total_chars: data.lyrics.length,\n returned_chars: slice.length,\n offset: args.offset,\n next_offset: nextOffset,\n truncated: nextOffset !== null,\n line_count: data.lyrics === \"\" ? 0 : data.lyrics.split(\"\\n\").length,\n highlight: args.highlight\n ? {\n word: args.highlight,\n found: match !== null,\n line_number: match?.lineNumber ?? null,\n line: match?.line ?? null,\n }\n : null,\n attribution,\n source: \"lyrics.com\" as const,\n notes,\n };\n\n // An offset beyond the words yields an empty body, which on its own reads\n // as a song that carries none. What happened is that the caller asked for\n // a position that does not exist, and only saying so tells the two apart.\n const pastTheEnd = args.offset > 0 && slice === \"\" && data.lyrics.length > 0;\n if (pastTheEnd) {\n notes.push(\n `offset=${args.offset} is past the end of a body of ${data.lyrics.length} characters. Call again with offset=0 to read it from the start.`,\n );\n }\n\n // The text mirror stays a pointer to the structured payload rather than a\n // second copy of the lyrics.\n const summary = [\n attribution,\n `${structured.line_count} lines, ${structured.total_chars} characters.`,\n howFarTheTextGoes(pastTheEnd, args.offset, nextOffset),\n whereTheWordIs(structured.highlight),\n ]\n .filter(Boolean)\n .join(\"\\n\");\n\n return ok(structured, truncate(`${summary}\\n\\n${slice}`, 4000), notes);\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * Two-pass keyword matcher.\n *\n * lyrics.com's own search is fuzzy: it returns title-only matches and loose\n * matches alongside genuine lyric-body hits. This matcher is what lets the\n * server promise that a returned song really contains the query.\n *\n * Pass 1 requires the keyword to be delimited on both sides, so \"coup\" does not\n * match \"beaucoup\". Pass 2 relaxes the right-hand side to a word start, so\n * \"enfant\" still matches \"enfants\". Pass 2 only runs when pass 1 finds nothing,\n * which keeps exact hits ranked ahead of prefix hits.\n */\n\n/**\n * Characters treated as word boundaries. Includes the typographic apostrophe,\n * which is common in French lyrics (\"l’enfant\").\n */\nconst WORD_DELIMITERS = \"[\\\\s,;:'\\\".!?()\\\\[\\\\]&’`@-]\";\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Index of `keyword` inside `text`, or -1 when absent.\n * The returned index points at the keyword itself, not at the leading delimiter.\n */\nexport function getKeywordIndex(keyword: string, text: string): number {\n if (!(keyword && text)) {\n return -1;\n }\n\n const escaped = escapeRegExp(keyword.toLowerCase());\n const haystack = text.toLowerCase();\n\n const passes = [\n new RegExp(`(^|${WORD_DELIMITERS})${escaped}(${WORD_DELIMITERS}|$)`),\n new RegExp(`(^|${WORD_DELIMITERS})${escaped}`),\n ];\n\n for (const pattern of passes) {\n const match = pattern.exec(haystack);\n if (match) {\n const leadingDelimiter = match[1] ?? \"\";\n return match.index + leadingDelimiter.length;\n }\n }\n\n return -1;\n}\n\nexport function containsWord(keyword: string, text: string): boolean {\n return getKeywordIndex(keyword, text) !== -1;\n}\n","/**\n * Excerpt extraction.\n *\n * Search results carry one short line showing where the query appears, never a\n * lyrics block. Excerpts stay small on purpose: they are enough for a model to\n * pick the right song, and search results are not the place to reproduce a\n * song's text.\n */\n\nimport { getKeywordIndex } from \"./keywordIndex.js\";\n\nexport interface ExcerptOptions {\n /** Hard cap on the returned excerpt. */\n maxChars?: number;\n}\n\nexport interface LineMatch {\n line: string;\n /** 1-based line number within the text. */\n lineNumber: number;\n}\n\n/** First line containing `keyword`, using the two-pass matcher. */\nexport function findMatchingLine(text: string, keyword: string): LineMatch | null {\n if (!(text && keyword)) {\n return null;\n }\n const lines = text.split(\"\\n\");\n for (let i = 0; i < lines.length; i += 1) {\n const line = lines[i] ?? \"\";\n if (line.trim() === \"\") {\n continue;\n }\n if (getKeywordIndex(keyword, line) !== -1) {\n return { line: line.trim(), lineNumber: i + 1 };\n }\n }\n return null;\n}\n\n/**\n * Excerpt of the line where `keyword` appears, trimmed to a window centred on\n * the match. Falls back to the first non-empty line when the keyword is absent.\n */\nexport function matchedLineExcerpt(\n text: string,\n keyword: string,\n options: ExcerptOptions = {},\n): string | null {\n const maxChars = options.maxChars ?? 160;\n if (!text) {\n return null;\n }\n\n const match = findMatchingLine(text, keyword);\n const line =\n match?.line ??\n text\n .split(\"\\n\")\n .find((candidate) => candidate.trim() !== \"\")\n ?.trim();\n if (!line) {\n return null;\n }\n if (line.length <= maxChars) {\n return line;\n }\n\n const keywordAt = match ? Math.max(getKeywordIndex(keyword, line), 0) : 0;\n const start = Math.max(0, keywordAt - Math.floor(maxChars / 2));\n const end = Math.min(line.length, start + maxChars);\n const slice = line.slice(start, end).trim();\n return `${start > 0 ? \"…\" : \"\"}${slice}${end < line.length ? \"…\" : \"\"}`;\n}\n","/**\n * How a tool's arguments are declared, and what happens to one that is not.\n *\n * An argument this server does not declare is a question it cannot answer.\n * Reading it and dropping it produces an answer computed on the defaults, which\n * a caller reads as the answer to what they asked. So an undeclared argument is\n * refused, and the refusal names it and offers the declared name when one is\n * close enough to be the one that was meant.\n *\n * A caller branches on the code a refusal opens with, and arguments are refused\n * along two paths: the code of a tool writes its own refusals, and the schema\n * refuses on its own before that code runs. Both open with the same code here,\n * so a caller reading the vocabulary finds it whichever path refused.\n */\n\nimport { z } from \"zod\";\n\n/** The code a caller branches on when the arguments cannot produce a request. */\nconst INVALID_INPUT = \"invalid_input\";\n\n/**\n * Zod's own wording. The error code is written in front of it.\n *\n * An error map may answer with an object or with nothing, and the English\n * locale answers every issue with a string, which is the type given here.\n */\nconst defaultMessage = z.core.locales.en().localeError as (issue: z.core.$ZodRawIssue) => string;\n\n/** Declare a tool's arguments, refusing anything outside the declaration. */\nexport function strictInput<Shape extends z.ZodRawShape>(shape: Shape) {\n const declared = Object.keys(shape);\n\n const refuse: z.core.$ZodErrorMap = (issue) =>\n issue.code === \"unrecognized_keys\"\n ? unknownArgumentMessage(issue.keys, declared)\n : `[${INVALID_INPUT}] ${defaultMessage(issue)}`;\n\n for (const argument of Object.values(shape)) {\n carryTheCode(argument, refuse);\n }\n\n return z.strictObject(shape, { error: refuse });\n}\n\n/**\n * Route every refusal a declared argument can raise through one error map.\n *\n * A bound such as `min` raises its issue from the check that holds it, and the\n * issue names that check as the instance whose error map applies, so the map\n * reaches it only by being written onto the check itself. Zod offers no copy\n * that keeps a check's identity, hence the assignment. It is confined to\n * schemas built for a declaration and handed to this function, which is the\n * whole of what a tool module does with them.\n */\nfunction carryTheCode(schema: z.core.$ZodType, refuse: z.core.$ZodErrorMap): void {\n const definition = schema._zod.def as {\n error?: unknown;\n checks?: Array<{ _zod: { def: { error?: unknown } } }>;\n innerType?: z.core.$ZodType;\n };\n\n definition.error = refuse;\n for (const check of definition.checks ?? []) {\n check._zod.def.error = refuse;\n }\n\n // A default, an optional or any other wrapper answers for its own shape and\n // leaves the bounds to the type it wraps.\n if (definition.innerType) {\n carryTheCode(definition.innerType, refuse);\n }\n}\n\nfunction unknownArgumentMessage(keys: readonly string[], declared: readonly string[]): string {\n const named = keys\n .map((key) => {\n const near = nearestArgument(key, declared);\n return near ? `'${key}' (did you mean '${near}'?)` : `'${key}'`;\n })\n .join(\", \");\n\n return (\n `[${INVALID_INPUT}] Unknown ${keys.length > 1 ? \"arguments\" : \"argument\"} ${named}. ` +\n `This tool takes: ${declared.join(\", \")}.`\n );\n}\n\n/**\n * The declared name a caller most plausibly meant, when there is one.\n *\n * Three readings, ordered by how much each claims: the same name written\n * differently, a name that opens or closes the other, and a name a couple of\n * typing slips away. Anything further is left unnamed, because a suggestion\n * that misses sends a caller to an argument answering a different question.\n */\nfunction nearestArgument(key: string, declared: readonly string[]): string | undefined {\n const flatten = (name: string) => name.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n const flat = flatten(key);\n if (flat.length === 0) {\n return undefined;\n }\n\n const sameName = declared.find((name) => flatten(name) === flat);\n if (sameName) {\n return sameName;\n }\n\n // Either name may be the longer one: a caller can qualify a name this tool\n // keeps plain, or shorten one it spells out.\n const overlapping = declared.find((name) => {\n const other = flatten(name);\n const [shorter, longer] = other.length < flat.length ? [other, flat] : [flat, other];\n // Two characters in common say nothing; three start to.\n return shorter.length >= 3 && (longer.startsWith(shorter) || longer.endsWith(shorter));\n });\n if (overlapping) {\n return overlapping;\n }\n\n let closest: string | undefined;\n let shortest = Number.POSITIVE_INFINITY;\n for (const name of declared) {\n const distance = editDistance(flat, flatten(name));\n if (distance < shortest) {\n shortest = distance;\n closest = name;\n }\n }\n\n // Up to a third of the name may differ. Past that the match is a guess.\n return shortest <= Math.max(1, Math.floor(flat.length / 3)) ? closest : undefined;\n}\n\n/**\n * Single-character insertions, deletions and substitutions between two words.\n *\n * One row of the matrix is kept at a time. The cell above comes from walking\n * that row, and the two other neighbours a cell needs, the one to its left and\n * the one diagonally above it, are carried forward from the step before, so\n * every value the walk uses is one it has just computed.\n */\nfunction editDistance(left: string, right: string): number {\n let previous = Array.from({ length: right.length + 1 }, (_, index) => index);\n let distance = right.length;\n\n for (let row = 1; row <= left.length; row += 1) {\n const current: number[] = [row];\n let diagonal = row - 1;\n let leftward = row;\n\n for (const [column, above] of previous.entries()) {\n if (column === 0) {\n continue;\n }\n const substitution = diagonal + (left[row - 1] === right[column - 1] ? 0 : 1);\n const cell = Math.min(substitution, above + 1, leftward + 1);\n current.push(cell);\n diagonal = above;\n leftward = cell;\n }\n\n previous = current;\n distance = leftward;\n }\n\n return distance;\n}\n","/**\n * Pieces shared by the three tools: result schema, error mapping, text mirrors.\n */\n\nimport { z } from \"zod\";\nimport { LyricsComError } from \"../errors.js\";\nimport type { SongResult } from \"../types.js\";\n\n/** Many MCP clients render only the text block, so it must stay readable on its own. */\nexport const MAX_TEXT_MIRROR_CHARS = 1500;\n\n/**\n * The last page a search will accept.\n *\n * Offering a page beyond this sends a caller into an argument the schema\n * refuses, so the number that bounds the input is the number an answer is\n * allowed to suggest.\n */\nexport const MAX_PAGE = 20;\n\nexport const songResultSchema = z.object({\n id: z.string().describe(\"lyrics.com song id. Pass this to get_lyrics.\"),\n title: z.string(),\n artist: z.string(),\n album: z.string().nullable(),\n year: z.number().int().nullable(),\n source_url: z.string().describe(\"Canonical lyrics.com page for this song.\"),\n excerpt: z\n .string()\n .nullable()\n .describe(\"Short lyric line showing where the query appears, when available.\"),\n});\n\nexport type SongResultOut = z.infer<typeof songResultSchema>;\n\nexport function toSongResultOut(song: SongResult, excerpt: string | null): SongResultOut {\n return {\n id: song.id,\n title: song.title,\n artist: song.artist,\n album: song.album,\n year: song.year,\n source_url: song.url,\n excerpt,\n };\n}\n\nexport interface ToolResult {\n // The SDK's CallToolResult carries an index signature for protocol extensions.\n [key: string]: unknown;\n content: Array<{ type: \"text\"; text: string }>;\n structuredContent?: Record<string, unknown>;\n isError?: boolean;\n}\n\n/**\n * Keep text from the site out of the shape this server's own lines take.\n *\n * A block ending with lines that open \"Note:\" gives a caller no way to tell one\n * of those from the same words inside a title someone else published.\n * Indenting such a line in the body keeps the two apart, and the structured\n * output still carries it exactly as it was.\n */\nfunction indentMarkerLines(body: string): string {\n return body.replace(/^(Note:)/gm, \" $1\");\n}\n\n/**\n * Build a result whose text block ends with its notes.\n *\n * The notes are what qualifies an answer: that an offset landed past the end,\n * that the site chose these results rather than matching them, that the words\n * came from this server's cache. A client rendering only the text reads an\n * unqualified answer without them.\n */\nexport function ok(\n structured: Record<string, unknown>,\n text: string,\n notes: string[] = [],\n): ToolResult {\n const trailer = notes.map((note) => `Note: ${note}`).join(\"\\n\");\n const budget = MAX_TEXT_MIRROR_CHARS - (trailer ? trailer.length + 2 : 0);\n const body = truncate(indentMarkerLines(text), Math.max(0, budget));\n\n return {\n content: [{ type: \"text\", text: trailer ? `${body}\\n\\n${trailer}` : body }],\n structuredContent: structured,\n };\n}\n\n/**\n * Error results carry no structuredContent: the SDK validates it against the\n * tool's declared output schema, which an error payload does not satisfy.\n */\nexport function toToolError(error: unknown): ToolResult {\n const known =\n error instanceof LyricsComError\n ? error\n : new LyricsComError(\"network_error\", error instanceof Error ? error.message : String(error));\n\n const lines = [`[${known.code}] ${known.message}`];\n if (known.details.hint) {\n lines.push(`Hint: ${known.details.hint}`);\n }\n if (known.details.url) {\n lines.push(`URL: ${known.details.url}`);\n }\n\n return { content: [{ type: \"text\", text: lines.join(\"\\n\") }], isError: true };\n}\n\nexport function truncate(text: string, maxChars: number): string {\n if (text.length <= maxChars) {\n return text;\n }\n return `${text.slice(0, maxChars - 1).trimEnd()}…`;\n}\n\n/** Compact listing used by both search tools. */\nexport function renderResultList(results: SongResultOut[]): string {\n return results\n .map((result, index) => {\n const year = result.year ? ` (${result.year})` : \"\";\n const album = result.album ? ` [${result.album}]` : \"\";\n const head = `${index + 1}. ${result.title} · ${result.artist}${year}${album} · id: ${result.id}`;\n return result.excerpt ? `${head}\\n \"${result.excerpt}\"` : head;\n })\n .join(\"\\n\");\n}\n","/**\n * search_lyrics: find songs by a word or phrase occurring in the lyrics.\n */\n\nimport { z } from \"zod\";\nimport type { LyricsComClient } from \"../lyricscom/client.js\";\nimport { dedupeSongs } from \"../text/dedupe.js\";\nimport { matchedLineExcerpt } from \"../text/excerpt.js\";\nimport { containsWord } from \"../text/keywordIndex.js\";\nimport type { SongResult } from \"../types.js\";\nimport { strictInput } from \"./arguments.js\";\nimport {\n MAX_PAGE,\n ok,\n renderResultList,\n songResultSchema,\n toSongResultOut,\n toToolError,\n} from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\n/** Verifying against full lyrics costs one page fetch per candidate. */\nconst MAX_FULL_VERIFICATIONS = 5;\n\n/**\n * The rows whose words actually carry the query, and how many did not.\n *\n * lyrics.com answers a search with rows whose snippet may not hold the words at\n * all, so each row is checked against the text there is: its snippet, or the\n * full lyrics when a caller asked for that and the cap allows another read. A\n * single unreadable song page is noted and stepped over rather than sinking the\n * whole search.\n */\nasync function keepTheOnesCarryingTheWords(\n client: LyricsComClient,\n args: SearchLyricsArgs,\n unique: readonly SongResult[],\n notes: string[],\n): Promise<{ kept: Array<{ song: SongResult; excerpt: string | null }>; filteredOut: number }> {\n const kept: Array<{ song: SongResult; excerpt: string | null }> = [];\n let filteredOut = 0;\n let fullChecks = 0;\n\n for (const song of unique) {\n if (kept.length >= args.limit) {\n break;\n }\n\n let text = song.snippet ?? \"\";\n if (args.verify === \"full\" && fullChecks < MAX_FULL_VERIFICATIONS) {\n fullChecks += 1;\n try {\n const page = await client.getSong({ id: song.id });\n if (page.data.hasLyrics) {\n text = page.data.lyrics;\n }\n } catch {\n notes.push(`Could not verify \"${song.title}\" against its full lyrics.`);\n }\n }\n\n const matches = args.verify === \"none\" || (text !== \"\" && containsWord(args.query, text));\n if (!matches) {\n filteredOut += 1;\n continue;\n }\n\n const excerpt =\n args.include_excerpt && text ? matchedLineExcerpt(text, args.query, { maxChars: 160 }) : null;\n kept.push({ song, excerpt });\n }\n\n return { kept, filteredOut };\n}\n\n/**\n * What to say about the rows past this page.\n *\n * The sentence may only name a page the schema would accept, or it sends a\n * caller into an argument that is refused.\n */\nfunction whatFollowsThisPage(hasMore: boolean, page: number): string {\n if (!hasMore) {\n return \"\";\n }\n if (page < MAX_PAGE) {\n return `\\n\\nMore results available: call again with page=${page + 1}.`;\n }\n return `\\n\\nlyrics.com holds more, but page ${MAX_PAGE} is as far as this tool reads. Narrow the query instead.`;\n}\n\nexport const searchLyricsDescription = [\n \"Search lyrics.com for songs whose lyrics contain a given word or phrase.\",\n \"Returns matching songs with artist, title, a short excerpt of the line where the word appears,\",\n \"and the lyrics.com id and URL needed to fetch the full text with get_lyrics.\",\n \"Use this when someone remembers a fragment of lyrics but not the song, or wants songs mentioning a word or theme.\",\n \"One call returns one page of lyrics.com results; to see more, increase 'page' rather than 'limit'.\",\n \"lyrics.com also returns loose and title-only matches, so results are filtered locally to keep only songs\",\n \"where the word genuinely appears in the lyrics. Set 'verify' to \\\"none\\\" to see the raw, unfiltered list.\",\n].join(\" \");\n\nexport const searchLyricsInput = strictInput({\n query: z\n .string()\n .min(1)\n .max(120)\n .describe(\"Word or short phrase to look for inside the lyrics, for example 'autrefois'.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(50)\n .default(10)\n .describe(\"Maximum songs to return from this page.\"),\n page: z\n .number()\n .int()\n .min(1)\n .max(20)\n .default(1)\n .describe(\"Result page on lyrics.com. Each page holds up to 24 raw results.\"),\n verify: z\n .enum([\"snippet\", \"full\", \"none\"])\n .default(\"snippet\")\n .describe(\n \"How to confirm the query really appears in the lyrics. 'snippet' checks the excerpt lyrics.com already \" +\n \"returned and costs nothing. 'full' fetches up to 5 song pages and checks the complete lyrics, which is \" +\n \"slow and can trigger rate limiting. 'none' disables filtering.\",\n ),\n include_excerpt: z\n .boolean()\n .default(true)\n .describe(\"Include the matching lyric line excerpt in each result.\"),\n});\n\nexport const searchLyricsOutputShape = {\n query: z.string(),\n page: z.number().int(),\n results: z.array(songResultSchema),\n raw_result_count: z\n .number()\n .int()\n .describe(\"Rows lyrics.com returned before local filtering and deduplication.\"),\n filtered_out: z\n .number()\n .int()\n .describe(\"Rows dropped because the query was not found in their lyrics.\"),\n has_more: z.boolean(),\n next_page: z.number().int().nullable(),\n source: z.literal(\"lyrics.com\"),\n notes: z\n .array(z.string())\n .describe(\"Caveats worth knowing, such as a cache hit or partial verification.\"),\n};\n\nexport interface SearchLyricsArgs {\n query: string;\n limit: number;\n page: number;\n verify: \"snippet\" | \"full\" | \"none\";\n include_excerpt: boolean;\n}\n\nexport async function runSearchLyrics(\n client: LyricsComClient,\n args: SearchLyricsArgs,\n): Promise<ToolResult> {\n try {\n const { data, cached } = await client.search(args.query, args.page);\n const notes: string[] = [];\n if (cached) {\n notes.push(\"Served from this server's short-lived in-memory cache.\");\n }\n\n const unique = dedupeSongs(data.results);\n const { kept, filteredOut } = await keepTheOnesCarryingTheWords(client, args, unique, notes);\n\n if (args.verify === \"full\" && unique.length > MAX_FULL_VERIFICATIONS) {\n notes.push(\n `Full verification is capped at ${MAX_FULL_VERIFICATIONS} songs per call to avoid rate limiting; ` +\n \"the remaining results were checked against their search excerpt.\",\n );\n }\n if (kept.length === 0 && data.rawCount > 0) {\n notes.push(\n \"lyrics.com returned results but none of them actually contain the query in their lyrics. \" +\n 'Try verify=\"none\" to see the raw matches.',\n );\n }\n\n const results = kept.map((entry) => toSongResultOut(entry.song, entry.excerpt));\n const structured = {\n query: args.query,\n page: args.page,\n results,\n raw_result_count: data.rawCount,\n filtered_out: filteredOut,\n has_more: data.hasMore,\n next_page: data.hasMore && args.page < MAX_PAGE ? args.page + 1 : null,\n source: \"lyrics.com\" as const,\n notes,\n };\n\n const header =\n results.length > 0\n ? `${results.length} song(s) on page ${args.page} for \"${args.query}\":`\n : `No song on page ${args.page} matched \"${args.query}\".`;\n // The footer may only name a page the schema would accept, or it sends a\n // caller into an argument that is refused.\n const footer = whatFollowsThisPage(data.hasMore, args.page);\n\n return ok(structured, `${header}\\n${renderResultList(results)}${footer}`);\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * Deduplication of search results.\n *\n * lyrics.com lists the same recording once per album it appears on, so a single\n * query routinely returns \"Title\", \"Title [Deluxe]\" and \"Title (Remastered)\" by\n * the same artist. Bracketed and parenthesised suffixes are stripped before\n * comparison so those collapse into one result.\n */\n\nimport type { SongResult } from \"../types.js\";\n\nexport function songKey(artist: string, title: string): string {\n const withoutSuffixes = title.replace(/\\s*[[(].*?[\\])]/g, \"\");\n const titleNorm = withoutSuffixes.replace(/\\s+/g, \" \").trim().toLowerCase();\n const artistNorm = artist.replace(/\\s+/g, \" \").trim().toLowerCase();\n return `${artistNorm}|${titleNorm}`;\n}\n\n/** Keep the first occurrence of each artist/title pair, preserving order. */\nexport function dedupeSongs<T extends SongResult>(songs: T[]): T[] {\n const seen = new Set<string>();\n const unique: T[] = [];\n for (const song of songs) {\n const key = songKey(song.artist, song.title);\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n unique.push(song);\n }\n return unique;\n}\n","/**\n * search_songs: find songs by title.\n *\n * lyrics.com has no title-only endpoint, so this runs the same search and ranks\n * and filters the rows by how well their title matches. The tool description\n * says so plainly rather than implying a precision the site does not offer.\n */\n\nimport { z } from \"zod\";\nimport type { LyricsComClient } from \"../lyricscom/client.js\";\nimport { dedupeSongs } from \"../text/dedupe.js\";\nimport type { SongResult } from \"../types.js\";\nimport { strictInput } from \"./arguments.js\";\nimport {\n MAX_PAGE,\n ok,\n renderResultList,\n songResultSchema,\n toSongResultOut,\n toToolError,\n} from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\n/**\n * What to say about the rows past this page.\n *\n * The sentence may only name a page the schema would accept, or it sends a\n * caller into an argument that is refused.\n */\nfunction whatFollowsThisPage(hasMore: boolean, page: number): string {\n if (!hasMore) {\n return \"\";\n }\n if (page < MAX_PAGE) {\n return `\\n\\nMore results available: call again with page=${page + 1}.`;\n }\n return `\\n\\nlyrics.com holds more, but page ${MAX_PAGE} is as far as this tool reads. Narrow the query instead.`;\n}\n\n/** The opening line, which says whether the site matched the title or merely offered rows. */\nfunction headerFor(found: number, loose: boolean, page: number, title: string): string {\n if (found === 0) {\n return `No song on page ${page} matched \"${title}\".`;\n }\n if (loose) {\n return `${found} song(s) lyrics.com offers on page ${page} for \"${title}\":`;\n }\n return `${found} song(s) on page ${page} matching \"${title}\":`;\n}\n\nexport const searchSongsDescription = [\n \"Find songs on lyrics.com by their title.\",\n \"Returns candidate songs with artist, album, year, and the lyrics.com id and URL.\",\n \"Use this when someone names a song and you need its id to call get_lyrics, or to tell covers and versions\",\n \"by different artists apart. An optional 'artist' narrows the list.\",\n \"lyrics.com searches titles and lyrics together, so results are ranked and filtered locally by title match;\",\n 'use match=\"strict\" to keep only genuine title matches.',\n].join(\" \");\n\nexport const searchSongsInput = strictInput({\n title: z.string().min(1).max(120).describe(\"Song title, or part of it.\"),\n artist: z\n .string()\n .max(120)\n .optional()\n .describe(\"Optional artist name, matched case-insensitively against each result.\"),\n limit: z.number().int().min(1).max(50).default(10),\n page: z.number().int().min(1).max(20).default(1),\n match: z\n .enum([\"loose\", \"strict\"])\n .default(\"loose\")\n .describe(\n \"'strict' keeps only results whose title matches the query as a whole title, ignoring suffixes such as \" +\n \"'(Remastered)'. 'loose' keeps anything lyrics.com returned, best match first.\",\n ),\n});\n\nexport const searchSongsOutputShape = {\n title: z.string(),\n artist_filter: z.string().nullable(),\n page: z.number().int(),\n results: z.array(songResultSchema),\n raw_result_count: z.number().int(),\n filtered_out: z.number().int(),\n has_more: z.boolean(),\n next_page: z.number().int().nullable(),\n source: z.literal(\"lyrics.com\"),\n notes: z.array(z.string()),\n};\n\nexport interface SearchSongsArgs {\n title: string;\n artist?: string;\n limit: number;\n page: number;\n match: \"loose\" | \"strict\";\n}\n\n/** Normalized form used for title comparison: no edition suffix, no punctuation noise. */\nfunction normalizeTitle(title: string): string {\n return title\n .replace(/\\s*[[(].*?[\\])]/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim()\n .toLowerCase();\n}\n\n/** 3 = same title, 2 = starts with the query, 1 = contains it, 0 = no title match. */\nexport function titleScore(candidate: string, query: string): number {\n const a = normalizeTitle(candidate);\n const b = normalizeTitle(query);\n if (!(a && b)) {\n return 0;\n }\n if (a === b) {\n return 3;\n }\n if (a.startsWith(b)) {\n return 2;\n }\n if (a.includes(b)) {\n return 1;\n }\n return 0;\n}\n\nexport async function runSearchSongs(\n client: LyricsComClient,\n args: SearchSongsArgs,\n): Promise<ToolResult> {\n try {\n const { data, cached } = await client.search(args.title, args.page);\n const notes: string[] = [];\n if (cached) {\n notes.push(\"Served from this server's short-lived in-memory cache.\");\n }\n\n const artistFilter = args.artist?.trim().toLowerCase() ?? null;\n const unique = dedupeSongs(data.results);\n\n const scored: Array<{ song: SongResult; score: number }> = [];\n let filteredOut = 0;\n\n for (const song of unique) {\n const score = titleScore(song.title, args.title);\n const artistOk = !artistFilter || song.artist.toLowerCase().includes(artistFilter);\n if (!artistOk || (args.match === \"strict\" && score < 2)) {\n filteredOut += 1;\n continue;\n }\n scored.push({ song, score });\n }\n\n // Stable sort by score, so equally scored rows keep lyrics.com's own order.\n scored.sort((a, b) => b.score - a.score);\n const results = scored.slice(0, args.limit).map((entry) => toSongResultOut(entry.song, null));\n\n if (results.length === 0 && data.rawCount > 0) {\n notes.push(\n args.match === \"strict\"\n ? 'lyrics.com returned results but none matched the title strictly. Try match=\"loose\".'\n : \"lyrics.com returned results but none matched the requested artist.\",\n );\n }\n\n const structured = {\n title: args.title,\n artist_filter: args.artist ?? null,\n page: args.page,\n results,\n raw_result_count: data.rawCount,\n filtered_out: filteredOut,\n has_more: data.hasMore,\n next_page: data.hasMore && args.page < MAX_PAGE ? args.page + 1 : null,\n source: \"lyrics.com\" as const,\n notes,\n };\n\n // In loose mode the site returns its own guesses, and a row whose title is\n // not the one asked for is among them. Calling the list a set of matches is\n // how one song's words end up quoted for another.\n const loose = args.match !== \"strict\";\n const header = headerFor(results.length, loose, args.page, args.title);\n // The footer may only name a page the schema would accept, or it sends a\n // caller into an argument that is refused.\n const footer = whatFollowsThisPage(data.hasMore, args.page);\n\n if (results.length > 0 && loose) {\n notes.push(\n `These are lyrics.com's own suggestions for \"${args.title}\", not rows whose title matches it. Check each title before quoting one, or ask again with match=\"strict\".`,\n );\n }\n\n return ok(structured, `${header}\\n${renderResultList(results)}${footer}`, notes);\n } catch (error) {\n return toToolError(error);\n }\n}\n"],"mappings":"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","names":["process","z","process","process","load","load","z","z","z","z","structured","z","z","z","whatFollowsThisPage","z","z","process"]}
|
package/dist/lyricscom/client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/config.ts","../../src/version.ts","../../src/lyricscom/cache.ts","../../src/errors.ts","../../src/lyricscom/parsers/detectThrottle.ts","../../src/lyricscom/rateLimiter.ts","../../src/lyricscom/http.ts","../../src/lyricscom/parsers/searchResults.ts","../../src/text/normalize.ts","../../src/lyricscom/urls.ts","../../src/lyricscom/parsers/selectors.ts","../../src/lyricscom/parsers/songPage.ts","../../src/lyricscom/client.ts"],"sourcesContent":["/**\n * Runtime configuration, read from environment variables.\n *\n * A bad value never crashes the process: an MCP server that dies at startup\n * because of a typo in a client config file is very hard to diagnose from the\n * host application, so invalid input is clamped and reported on stderr.\n */\n\nimport process from \"node:process\";\nimport { PKG_VERSION, REPO_URL } from \"./version.js\";\n\nexport type LogLevel = \"silent\" | \"error\" | \"info\" | \"debug\";\n\nexport interface Config {\n userAgent: string;\n minIntervalMs: number;\n timeoutMs: number;\n maxRetries: number;\n cacheTtlMs: number;\n cacheMaxEntries: number;\n logLevel: LogLevel;\n}\n\n/**\n * Identifies the client honestly. lyrics.com serves this fine; it blocks\n * generic tool agents such as `curl/x.y.z`, which is what the override is for.\n */\nexport const DEFAULT_USER_AGENT = `mcp-lyricscom/${PKG_VERSION} (+${REPO_URL})`;\n\nexport const DEFAULTS = {\n minIntervalMs: 1100,\n timeoutMs: 15_000,\n maxRetries: 3,\n cacheTtlMs: 15 * 60 * 1000,\n cacheMaxEntries: 200,\n logLevel: \"error\" as LogLevel,\n};\n\n/**\n * Floor on the request interval, enforced regardless of configuration.\n *\n * The pacing is what keeps this client from behaving like a crawler against a\n * site that has no API and no published rate limit. Leaving it configurable down\n * to zero would make the politeness of every installation depend on whoever\n * edited a JSON file, so a value below this floor is refused and the default is\n * used instead.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 500;\n\nconst LOG_LEVELS: LogLevel[] = [\"silent\", \"error\", \"info\", \"debug\"];\n\ninterface NumericRange {\n min: number;\n max: number;\n fallback: number;\n}\n\nfunction readNumber(name: string, env: NodeJS.ProcessEnv, range: NumericRange): number {\n const raw = env[name];\n if (raw === undefined || raw.trim() === \"\") {\n return range.fallback;\n }\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`${name}=\"${raw}\" is not a number, using ${range.fallback}`);\n return range.fallback;\n }\n const clamped = Math.min(range.max, Math.max(range.min, Math.round(parsed)));\n if (clamped !== Math.round(parsed)) {\n warn(`${name}=${raw} is out of range, clamped to ${clamped}`);\n }\n return clamped;\n}\n\nfunction warn(message: string): void {\n process.stderr.write(`[mcp-lyricscom] ${message}\\n`);\n}\n\n/**\n * Read the request interval, refusing anything below the floor.\n *\n * A value under the floor falls back to the default rather than to the floor\n * itself: someone who set 0 was not asking for 500, they were asking for no\n * pacing at all, and the safe reading of that request is to ignore it.\n */\nfunction readInterval(env: NodeJS.ProcessEnv): number {\n const raw = env.LYRICSCOM_MIN_INTERVAL_MS;\n if (raw === undefined || raw.trim() === \"\") {\n return DEFAULTS.minIntervalMs;\n }\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`LYRICSCOM_MIN_INTERVAL_MS=\"${raw}\" is not a number, using ${DEFAULTS.minIntervalMs}ms`);\n return DEFAULTS.minIntervalMs;\n }\n\n const rounded = Math.round(parsed);\n if (rounded < MIN_ALLOWED_INTERVAL_MS) {\n warn(\n `LYRICSCOM_MIN_INTERVAL_MS=${raw} is below the ${MIN_ALLOWED_INTERVAL_MS}ms floor and was ignored; ` +\n `using ${DEFAULTS.minIntervalMs}ms. This floor keeps the client from hammering lyrics.com.`,\n );\n return DEFAULTS.minIntervalMs;\n }\n\n return Math.min(60_000, rounded);\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const rawUserAgent = env.LYRICSCOM_USER_AGENT?.trim();\n const rawLogLevel = env.LYRICSCOM_LOG_LEVEL?.trim().toLowerCase();\n\n let logLevel = DEFAULTS.logLevel;\n if (rawLogLevel) {\n if (LOG_LEVELS.includes(rawLogLevel as LogLevel)) {\n logLevel = rawLogLevel as LogLevel;\n } else {\n warn(`LYRICSCOM_LOG_LEVEL=\"${rawLogLevel}\" is unknown, using \"${DEFAULTS.logLevel}\"`);\n }\n }\n\n return {\n userAgent: rawUserAgent || DEFAULT_USER_AGENT,\n minIntervalMs: readInterval(env),\n timeoutMs: readNumber(\"LYRICSCOM_TIMEOUT_MS\", env, {\n min: 1000,\n max: 120_000,\n fallback: DEFAULTS.timeoutMs,\n }),\n maxRetries: readNumber(\"LYRICSCOM_MAX_RETRIES\", env, {\n min: 0,\n max: 10,\n fallback: DEFAULTS.maxRetries,\n }),\n cacheTtlMs: readNumber(\"LYRICSCOM_CACHE_TTL_MS\", env, {\n min: 0,\n max: 24 * 60 * 60 * 1000,\n fallback: DEFAULTS.cacheTtlMs,\n }),\n cacheMaxEntries: readNumber(\"LYRICSCOM_CACHE_MAX_ENTRIES\", env, {\n min: 0,\n max: 10_000,\n fallback: DEFAULTS.cacheMaxEntries,\n }),\n logLevel,\n };\n}\n\nconst LEVEL_RANK: Record<LogLevel, number> = { silent: 0, error: 1, info: 2, debug: 3 };\n\n/**\n * Logs go to stderr without exception. On a stdio transport, stdout carries the\n * protocol and any stray write there corrupts the session.\n */\nexport function createLogger(level: LogLevel) {\n const emit = (at: LogLevel, message: string) => {\n if (LEVEL_RANK[level] >= LEVEL_RANK[at]) {\n process.stderr.write(`[mcp-lyricscom] ${message}\\n`);\n }\n };\n return {\n error: (message: string) => emit(\"error\", message),\n info: (message: string) => emit(\"info\", message),\n debug: (message: string) => emit(\"debug\", message),\n };\n}\n\nexport type Logger = ReturnType<typeof createLogger>;\n","/**\n * Kept in sync with package.json. The User-Agent embeds it, so it must not\n * drift; the release checklist bumps both together.\n */\nexport const PKG_VERSION = \"2.0.1\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-lyricscom\";\n","/**\n * In-memory TTL + LRU cache.\n *\n * Nothing is written to disk. The cache exists so that repeated questions about\n * the same song inside one conversation do not hit lyrics.com again, not to\n * build a local copy of the site.\n *\n * A Map iterates in insertion order, so re-inserting on every hit is enough to\n * make the first key the least recently used one.\n */\n\ninterface Entry<V> {\n value: V;\n expiresAt: number;\n}\n\nexport class TtlLruCache<V> {\n private readonly store = new Map<string, Entry<V>>();\n\n constructor(\n private readonly maxEntries: number,\n private readonly ttlMs: number,\n ) {}\n\n get(key: string): V | undefined {\n const entry = this.store.get(key);\n if (!entry) {\n return undefined;\n }\n if (Date.now() >= entry.expiresAt) {\n this.store.delete(key);\n return undefined;\n }\n this.store.delete(key);\n this.store.set(key, entry);\n return entry.value;\n }\n\n set(key: string, value: V): void {\n if (this.maxEntries <= 0 || this.ttlMs <= 0) {\n return;\n }\n this.store.delete(key);\n this.store.set(key, { value, expiresAt: Date.now() + this.ttlMs });\n while (this.store.size > this.maxEntries) {\n const oldest = this.store.keys().next();\n if (oldest.done) {\n break;\n }\n this.store.delete(oldest.value);\n }\n }\n\n get size(): number {\n return this.store.size;\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","/**\n * Error taxonomy surfaced to the calling model.\n *\n * The point of this file is that an LLM must be able to tell \"lyrics.com pushed\n * back on me\" apart from \"there is no such song\". The reference implementation\n * this server replaces collapsed every failure into an empty list, which made\n * rate limiting look like an absence of results.\n */\n\nexport type ErrorCode =\n | \"throttled\"\n | \"blocked_user_agent\"\n | \"not_found\"\n | \"invalid_input\"\n | \"parse_failure\"\n | \"network_error\"\n | \"timeout\";\n\nexport interface ErrorDetails {\n url?: string;\n status?: number;\n retryAfterMs?: number;\n hint?: string;\n}\n\nexport class LyricsComError extends Error {\n constructor(\n readonly code: ErrorCode,\n message: string,\n readonly details: ErrorDetails = {},\n ) {\n super(message);\n this.name = \"LyricsComError\";\n }\n}\n\nconst ISSUES_URL = \"https://github.com/smeet666/mcp-lyricscom/issues\";\n\nexport function throttled(url: string, retryAfterMs: number): LyricsComError {\n return new LyricsComError(\n \"throttled\",\n \"lyrics.com is rate limiting this client. It answered with an empty response instead of a page. \" +\n \"This does NOT mean there are no results for this query.\",\n {\n url,\n retryAfterMs,\n hint:\n \"Wait at least a minute before calling the same tool again with the same arguments. Once lyrics.com \" +\n \"starts throttling, the window commonly lasts several minutes, so retrying immediately will fail again. \" +\n \"If it keeps happening, raise LYRICSCOM_MIN_INTERVAL_MS in your MCP client configuration.\",\n },\n );\n}\n\nexport function blockedUserAgent(url: string): LyricsComError {\n return new LyricsComError(\n \"blocked_user_agent\",\n \"lyrics.com refused the request because of the User-Agent this server sends (HTTP 403).\",\n {\n url,\n status: 403,\n hint:\n \"Set the LYRICSCOM_USER_AGENT environment variable in your MCP client configuration to a different \" +\n \"value. See the Configuration section of the project README.\",\n },\n );\n}\n\nexport function notFound(url: string): LyricsComError {\n return new LyricsComError(\"not_found\", \"lyrics.com has no page at this address.\", {\n url,\n status: 404,\n hint: \"Check the song id. Ids come from the search tools; they are not stable to guess by hand.\",\n });\n}\n\nexport function parseFailure(url: string, what: string): LyricsComError {\n return new LyricsComError(\n \"parse_failure\",\n `The page loaded but the expected lyrics.com markup was not found (${what}). The site layout may have changed.`,\n { url, hint: `Please report this, with the query you used, at ${ISSUES_URL}` },\n );\n}\n\nexport function invalidInput(message: string, hint?: string): LyricsComError {\n return new LyricsComError(\"invalid_input\", message, hint ? { hint } : {});\n}\n","/**\n * Response classification.\n *\n * lyrics.com does not answer rate limiting with 429. It answers with HTTP 202\n * and a zero-byte body, which a naive client reads as a successful but empty\n * page. Every response therefore goes through this classifier before any\n * parsing happens.\n */\n\nconst CLOSING_HTML_TAG = /<\\/html>/i;\n\nexport type ResponseVerdict =\n | { kind: \"ok\"; html: string }\n | { kind: \"throttled\"; reason: ThrottleReason }\n | { kind: \"blocked\"; status: number }\n | { kind: \"not-found\" }\n | { kind: \"server-error\"; status: number };\n\nexport type ThrottleReason =\n | \"status-202\"\n | \"status-429\"\n | \"status-503\"\n | \"empty-body\"\n | \"short-body\";\n\n/**\n * A real search page weighs about 187 KB and a song page 60 KB or more, so a\n * body this small is never a genuine lyrics.com page.\n */\nexport const MIN_PLAUSIBLE_HTML = 2000;\n\nexport function classifyResponse(status: number, body: string): ResponseVerdict {\n if (status === 202) {\n return { kind: \"throttled\", reason: \"status-202\" };\n }\n if (status === 429) {\n return { kind: \"throttled\", reason: \"status-429\" };\n }\n if (status === 503) {\n return { kind: \"throttled\", reason: \"status-503\" };\n }\n if (status === 403) {\n return { kind: \"blocked\", status };\n }\n if (status === 404) {\n return { kind: \"not-found\" };\n }\n if (status >= 500) {\n return { kind: \"server-error\", status };\n }\n\n const trimmed = body.trim();\n if (trimmed.length === 0) {\n return { kind: \"throttled\", reason: \"empty-body\" };\n }\n if (trimmed.length < MIN_PLAUSIBLE_HTML && !CLOSING_HTML_TAG.test(trimmed)) {\n return { kind: \"throttled\", reason: \"short-body\" };\n }\n\n return { kind: \"ok\", html: body };\n}\n","/**\n * Serial request queue with an adaptive minimum interval.\n *\n * lyrics.com tolerates a steady trickle but stops answering after a short burst,\n * so requests are run one at a time with a floor on the gap between starts.\n * When the site does push back, the interval doubles and then decays back down\n * as requests succeed, which recovers faster than a fixed delay and behaves\n * better than hammering at a constant rate.\n */\n\nexport interface RateLimiterOptions {\n minIntervalMs: number;\n maxIntervalMs?: number;\n}\n\nconst DEFAULT_MAX_INTERVAL_MS = 10_000;\n\nexport class RateLimiter {\n private readonly baseIntervalMs: number;\n private readonly maxIntervalMs: number;\n private intervalMs: number;\n /** Tail of the queue: each task chains onto the previous one. */\n private tail: Promise<unknown> = Promise.resolve();\n private lastStart = 0;\n\n constructor(options: RateLimiterOptions) {\n this.baseIntervalMs = Math.max(0, options.minIntervalMs);\n this.maxIntervalMs = Math.max(\n this.baseIntervalMs,\n options.maxIntervalMs ?? DEFAULT_MAX_INTERVAL_MS,\n );\n this.intervalMs = this.baseIntervalMs;\n }\n\n get currentIntervalMs(): number {\n return this.intervalMs;\n }\n\n /** Queue a task. Tasks run in call order, one at a time. */\n schedule<T>(task: () => Promise<T>): Promise<T> {\n const run = this.tail.then(async () => task());\n // The queue must keep draining even when a task rejects.\n this.tail = run.then(\n () => undefined,\n () => undefined,\n );\n return run;\n }\n\n /**\n * Wait for this request's slot, then claim it.\n *\n * Called once per upstream request rather than once per task, because a task\n * runs a whole retry chain: stamping only its start would let the next task\n * follow the chain's last request with no gap at all.\n */\n async beforeRequest(): Promise<void> {\n await this.waitForSlot();\n this.lastStart = Date.now();\n }\n\n /** Called after the site signals it is under load. */\n penalize(): void {\n const next = this.intervalMs === 0 ? 250 : this.intervalMs * 2;\n this.intervalMs = Math.min(this.maxIntervalMs, next);\n }\n\n /** Called after a success, so a single hiccup does not slow things down forever. */\n relax(): void {\n this.intervalMs = Math.max(this.baseIntervalMs, Math.floor(this.intervalMs * 0.75));\n }\n\n private async waitForSlot(): Promise<void> {\n if (this.intervalMs === 0 || this.lastStart === 0) {\n return;\n }\n const elapsed = Date.now() - this.lastStart;\n // Clamped to the interval: a clock stepped backwards, by NTP or a resumed\n // virtual machine, would otherwise make this wait for the size of the step,\n // and the queue is serial so every pending request would wait behind it.\n const remaining = Math.min(this.intervalMs, this.intervalMs - elapsed);\n if (remaining > 0) {\n await sleep(remaining);\n }\n }\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * HTTP layer: one GET, classified, with backoff.\n *\n * The retry policy is domain specific rather than generic, because the failure\n * this server exists to handle does not look like a failure at the transport\n * level: lyrics.com answers a throttled request with HTTP 202 and an empty\n * body. Only `classifyResponse` can tell that apart from a real page.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport { blockedUserAgent, LyricsComError, notFound, throttled } from \"../errors.js\";\nimport { classifyResponse } from \"./parsers/detectThrottle.js\";\nimport { type RateLimiter, sleep } from \"./rateLimiter.js\";\n\nconst BACKOFF_BASE_MS = 2000;\nconst BACKOFF_FACTOR = 2;\nconst BACKOFF_MAX_MS = 20_000;\n\n/** Exponential backoff with jitter, so parallel clients do not resynchronise. */\nexport function backoffDelay(attempt: number, random: () => number = Math.random): number {\n const uncapped = BACKOFF_BASE_MS * BACKOFF_FACTOR ** attempt;\n const capped = Math.min(BACKOFF_MAX_MS, uncapped);\n return Math.round(capped * (0.5 + random() * 0.5));\n}\n\nexport interface HttpDeps {\n config: Config;\n limiter: RateLimiter;\n logger: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/**\n * Fetch one page as HTML, retrying transient conditions.\n *\n * The whole retry loop, backoff sleeps included, runs inside a single limiter\n * slot. Waiting outside it would let a queued request slip into the same\n * throttling window that the current one is backing away from.\n */\nexport async function fetchHtml(url: string, deps: HttpDeps): Promise<string> {\n const { config, limiter, logger } = deps;\n const doFetch = deps.fetchImpl ?? fetch;\n\n return await limiter.schedule(async () => {\n let lastError: LyricsComError | undefined;\n\n // Set when the site says how long to stay away; it replaces our own guess\n // for the next attempt. Applied here rather than where it is read, so no\n // wait is ever served after the last attempt, when nobody would use it.\n let askedWaitMs: number | null = null;\n\n for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {\n if (attempt > 0) {\n const delay = Math.min(askedWaitMs ?? backoffDelay(attempt - 1), BACKOFF_MAX_MS);\n askedWaitMs = null;\n logger.info(`retry ${attempt}/${config.maxRetries} in ${delay}ms for ${url}`);\n await sleep(delay);\n }\n\n let status: number;\n let body: string;\n let retryAfterMs: number | null = null;\n try {\n await limiter.beforeRequest();\n const response = await doFetch(url, {\n headers: {\n \"User-Agent\": config.userAgent,\n Accept: \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n \"Accept-Language\": \"en-US,en;q=0.9,fr;q=0.8\",\n },\n redirect: \"follow\",\n signal: AbortSignal.timeout(config.timeoutMs),\n });\n status = response.status;\n retryAfterMs = parseRetryAfter(response.headers.get(\"retry-after\"));\n body = await response.text();\n } catch (error) {\n lastError = asTransportError(error, url);\n logger.debug(`${lastError.code} for ${url}: ${lastError.message}`);\n continue;\n }\n\n const verdict = classifyResponse(status, body);\n switch (verdict.kind) {\n case \"ok\":\n limiter.relax();\n return verdict.html;\n\n case \"throttled\":\n limiter.penalize();\n logger.info(\n `throttled (${verdict.reason}) on ${url}, interval now ${limiter.currentIntervalMs}ms`,\n );\n // A server that says when to come back knows better than our own guess.\n askedWaitMs = retryAfterMs;\n lastError = throttled(url, retryAfterMs ?? backoffDelay(attempt));\n continue;\n\n case \"server-error\":\n lastError = new LyricsComError(\n \"network_error\",\n `lyrics.com returned HTTP ${verdict.status}.`,\n {\n url,\n status: verdict.status,\n },\n );\n continue;\n\n // Retrying these would only repeat the same answer.\n case \"blocked\":\n throw blockedUserAgent(url);\n case \"not-found\":\n throw notFound(url);\n\n // Every kind the classifier declares is handled above, and the linter\n // holds that. This clause is the runtime backstop: a response nothing\n // above matched leaves as an error rather than as a silent retry.\n default:\n throw new LyricsComError(\n \"network_error\",\n `lyrics.com returned a response this client cannot classify for ${url}.`,\n { url },\n );\n }\n }\n\n throw lastError ?? new LyricsComError(\"network_error\", `Could not fetch ${url}.`, { url });\n });\n}\n\n/** `Retry-After` carries either seconds or an HTTP date. */\nfunction parseRetryAfter(raw: string | null): number | null {\n if (!raw) {\n return null;\n }\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) {\n return Math.round(seconds * 1000);\n }\n const when = Date.parse(raw);\n if (Number.isNaN(when)) {\n return null;\n }\n return Math.max(0, when - Date.now());\n}\n\nfunction asTransportError(error: unknown, url: string): LyricsComError {\n if (error instanceof LyricsComError) {\n return error;\n }\n const name = error instanceof Error ? error.name : \"\";\n if (name === \"TimeoutError\" || name === \"AbortError\") {\n return new LyricsComError(\"timeout\", \"lyrics.com did not answer in time.\", {\n url,\n hint: \"Raise LYRICSCOM_TIMEOUT_MS if this happens often on a slow connection.\",\n });\n }\n const message = error instanceof Error ? error.message : String(error);\n return new LyricsComError(\"network_error\", `Could not reach lyrics.com: ${message}`, { url });\n}\n","/**\n * Parser for a lyrics.com search results page.\n *\n * Page 1 (`/lyrics/{term}`) and later pages (`/serp.php?st=...&p=N`) share the\n * same markup, so one parser covers both.\n */\n\nimport process from \"node:process\";\nimport { load } from \"cheerio\";\nimport { parseFailure } from \"../../errors.js\";\nimport type { SearchPage, SongResult } from \"../../types.js\";\nimport { cleanInlineText, cleanLyricsText } from \"../../text/normalize.js\";\nimport { extractSongId, RESULTS_PER_PAGE, toAbsoluteUrl } from \"../urls.js\";\nimport { SEL } from \"./selectors.js\";\n\n/** A year a record can plausibly carry, from the 1800s to this century. */\nconst YEAR = /\\b(1[89]\\d{2}|20\\d{2})\\b/;\n\nexport interface ParseSearchOptions {\n page: number;\n /** Reported in errors so a failure points at the request that caused it. */\n url: string;\n}\n\nexport function parseSearchResults(html: string, options: ParseSearchOptions): SearchPage {\n const $ = load(html);\n const rows = $(SEL.serpItem);\n\n if (rows.length === 0) {\n // An empty result set still renders the page chrome. Its absence means the\n // response was not the page we asked for, which must not be reported as\n // \"no songs found\".\n if ($(SEL.serpShell).length === 0) {\n throw parseFailure(options.url, \"no result rows and no recognizable search page structure\");\n }\n return { results: [], page: options.page, rawCount: 0, hasMore: false };\n }\n\n const results: SongResult[] = [];\n let skipped = 0;\n\n rows.each((_, element) => {\n const row = $(element);\n const titleLink = row.find(SEL.resultTitle).first();\n const href = titleLink.attr(\"href\");\n const title = cleanInlineText(titleLink.text());\n const artist = cleanInlineText(row.find(SEL.resultArtist).first().text());\n\n const id = href ? extractSongId(href) : null;\n if (!(id && title && artist)) {\n skipped += 1;\n return;\n }\n\n const album = cleanInlineText(row.find(SEL.resultAlbum).first().text()) || null;\n const year = parseYear(row.find(SEL.resultYear).first().text());\n const snippetRaw = row.find(SEL.resultSnippet).first().text();\n const snippet = snippetRaw ? cleanLyricsText(snippetRaw) || null : null;\n\n results.push({\n id,\n url: toAbsoluteUrl(href as string),\n title,\n artist,\n album,\n year,\n snippet,\n });\n });\n\n if (results.length === 0) {\n throw parseFailure(options.url, `${rows.length} result rows matched but none could be read`);\n }\n if (skipped > 0) {\n process.stderr.write(\n `[mcp-lyricscom] skipped ${skipped} unreadable result rows on ${options.url}\\n`,\n );\n }\n\n return {\n results,\n page: options.page,\n rawCount: rows.length,\n hasMore: rows.length >= RESULTS_PER_PAGE,\n };\n}\n\n/** Year cells look like \"1971\" but are sometimes decorated or empty. */\nfunction parseYear(raw: string): number | null {\n const match = YEAR.exec(raw);\n if (!match?.[1]) {\n return null;\n }\n return Number.parseInt(match[1], 10);\n}\n","/**\n * Text cleaning for scraped content: HTML entities, mojibake, whitespace.\n */\n\n/** The letters UTF-8 read as Latin-1 leaves behind. */\nconst MOJIBAKE_MARKS = /[ÃÂ]/;\n\nconst HTML_ENTITIES: Record<string, string> = {\n amp: \"&\",\n lt: \"<\",\n gt: \">\",\n quot: '\"',\n apos: \"'\",\n nbsp: \" \",\n hellip: \"…\",\n mdash: \"—\",\n ndash: \"–\",\n rsquo: \"’\",\n lsquo: \"‘\",\n ldquo: \"“\",\n rdquo: \"”\",\n};\n\n/** Decode named and numeric HTML entities. */\nexport function unescapeHtml(text: string): string {\n if (!text) {\n return text;\n }\n return text.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (match, entity: string) => {\n if (entity.startsWith(\"#\")) {\n const isHex = entity[1] === \"x\" || entity[1] === \"X\";\n const codePoint = Number.parseInt(isHex ? entity.slice(2) : entity.slice(1), isHex ? 16 : 10);\n if (Number.isNaN(codePoint) || codePoint < 0 || codePoint > 0x10ffff) {\n return match;\n }\n try {\n return String.fromCodePoint(codePoint);\n } catch {\n return match;\n }\n }\n return HTML_ENTITIES[entity.toLowerCase()] ?? match;\n });\n}\n\n/**\n * Repair UTF-8 bytes that were decoded as Latin-1, which turns \"é\" into \"é\".\n * lyrics.com serves French titles that occasionally arrive in this state.\n * Re-encoding through latin1 recovers the original text; the round trip is only\n * kept when it removes replacement characters, so correct text is never touched.\n */\nexport function fixEncodingIssues(text: string): string {\n if (!(text && MOJIBAKE_MARKS.test(text))) {\n return text;\n }\n try {\n const repaired = Buffer.from(text, \"latin1\").toString(\"utf8\");\n if (repaired.includes(\"�\")) {\n return text;\n }\n return repaired;\n } catch {\n return text;\n }\n}\n\n/** Collapse runs of whitespace and trim. */\nexport function collapseWhitespace(text: string): string {\n return text.replace(/\\s+/g, \" \").trim();\n}\n\n/** Full cleaning pass for a scraped title or artist name. */\nexport function cleanInlineText(text: string): string {\n if (!text) {\n return \"\";\n }\n return collapseWhitespace(fixEncodingIssues(unescapeHtml(text)));\n}\n\n/**\n * Normalize a lyrics block: trim each line, drop leading and trailing blank\n * lines, and collapse runs of blank lines to a single verse separator.\n */\nexport function cleanLyricsText(text: string): string {\n if (!text) {\n return \"\";\n }\n const normalized = fixEncodingIssues(unescapeHtml(text)).replace(/\\r\\n?/g, \"\\n\");\n const lines = normalized.split(\"\\n\").map((line) => line.trim());\n\n const out: string[] = [];\n for (const line of lines) {\n if (line === \"\" && (out.length === 0 || out.at(-1) === \"\")) {\n continue;\n }\n out.push(line);\n }\n while (out.length > 0 && out.at(-1) === \"\") {\n out.pop();\n }\n return out.join(\"\\n\");\n}\n","/**\n * URL construction and validation for lyrics.com.\n */\n\nimport { invalidInput } from \"../errors.js\";\n\nconst ABSOLUTE_URL = /^https?:\\/\\//i;\nconst DIGITS_ONLY = /^\\d+$/;\n\nexport const BASE_URL = \"https://www.lyrics.com\";\n\n/** lyrics.com renders 24 rows per search page. */\nexport const RESULTS_PER_PAGE = 24;\n\n/**\n * Song page paths come in two shapes: the current `/lyric-lf/{id}/...` and the\n * older `/lyric/{id}/...` still present in archived links.\n */\nexport const SONG_HREF_RE = /^\\/lyric(?:-lf)?\\/(\\d+)\\//;\n\nconst ALLOWED_HOSTS = new Set([\"lyrics.com\", \"www.lyrics.com\"]);\n\n/**\n * Percent-encode a search term the way lyrics.com expects, with spaces as `+`.\n */\nfunction encodeTerm(term: string): string {\n return encodeURIComponent(term).replace(/%20/g, \"+\");\n}\n\n/**\n * Page 1 lives at a path, later pages at a query string. Both return the same\n * markup, so the parser does not need to know which one produced it.\n */\nexport function buildSearchUrl(term: string, page: number): string {\n const encoded = encodeTerm(term);\n if (page <= 1) {\n return `${BASE_URL}/lyrics/${encoded}`;\n }\n return `${BASE_URL}/serp.php?st=${encoded}&p=${page}`;\n}\n\nexport function buildSongUrl(id: string): string {\n return `${BASE_URL}/lyric-lf/${id}/`;\n}\n\n/** True only for lyrics.com itself, so a hostile URL cannot be used as a proxy. */\nexport function isLyricsComHost(rawUrl: string): boolean {\n let parsed: URL;\n try {\n parsed = new URL(rawUrl);\n } catch {\n return false;\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n return false;\n }\n return ALLOWED_HOSTS.has(parsed.hostname.toLowerCase());\n}\n\n/** Extract the numeric song id from a lyrics.com song path or absolute URL. */\nexport function extractSongId(hrefOrUrl: string): string | null {\n let path = hrefOrUrl;\n if (ABSOLUTE_URL.test(hrefOrUrl)) {\n if (!isLyricsComHost(hrefOrUrl)) {\n return null;\n }\n path = new URL(hrefOrUrl).pathname;\n }\n if (!path.startsWith(\"/\")) {\n path = `/${path}`;\n }\n const match = SONG_HREF_RE.exec(path);\n return match?.[1] ?? null;\n}\n\n/**\n * Resolve the `id` / `url` pair accepted by get_lyrics into a canonical\n * request URL. `id` wins when both are given.\n */\nexport function resolveSongRef(input: { id?: string; url?: string }): { id: string; url: string } {\n if (input.id) {\n if (!DIGITS_ONLY.test(input.id)) {\n throw invalidInput(\n `\"${input.id}\" is not a lyrics.com song id.`,\n \"Ids are digits only, as returned by the search tools.\",\n );\n }\n return { id: input.id, url: buildSongUrl(input.id) };\n }\n\n if (input.url) {\n if (!isLyricsComHost(input.url)) {\n throw invalidInput(\n \"Only www.lyrics.com URLs are accepted.\",\n \"Pass the source_url returned by search_lyrics or search_songs, or use the numeric id instead.\",\n );\n }\n const id = extractSongId(input.url);\n if (!id) {\n throw invalidInput(\n \"That lyrics.com URL is not a song page.\",\n \"Song pages look like https://www.lyrics.com/lyric-lf/1234567/Artist/Title\",\n );\n }\n return { id, url: input.url };\n }\n\n throw invalidInput(\"Either 'id' or 'url' must be provided.\");\n}\n\n/** Absolute URL for a href scraped out of a result row. */\nexport function toAbsoluteUrl(href: string): string {\n return new URL(href, BASE_URL).toString();\n}\n","/**\n * Every lyrics.com CSS selector lives here, so a site redesign is a one-file fix.\n *\n * Selectors deliberately target a single stable class rather than a full class\n * list (`div.sec-lyric`, not `div.sec-lyric.clearfix`), so that a layout tweak\n * adding or removing a utility class does not break extraction.\n */\n\nexport const SEL = {\n /** One search result row. */\n serpItem: \"div.sec-lyric\",\n /**\n * Page chrome that is present whether or not there are results. Used to tell\n * \"zero results\" apart from \"the markup changed\", which must not look alike.\n */\n serpShell: \"#lyric-search, .lyric-search, .pagination, #main-content, form[action*='serp']\",\n resultTitle: \"p.lyric-meta-title a\",\n /**\n * Rows tied to an album expose the artist under `lyric-meta-album-artist`,\n * rows without one under `lyric-meta-artists`. Both shapes appear on the same\n * page, so missing the second silently drops results.\n */\n resultArtist: \"p.lyric-meta-album-artist a, p.lyric-meta-artists a\",\n resultAlbum: \"p.lyric-meta-album\",\n resultYear: \"p.lyric-meta-album-year\",\n resultSnippet: \"pre.lyric-body\",\n\n /** Full lyrics on a song page, with the historical fallback. */\n songBody: \"pre#lyric-body-text, pre.lyric-body\",\n /** Marker for a valid song page that simply has no lyrics on file. */\n songNoData: \"div.lyric-no-data\",\n songTitle: \"#lyric-title-text, .lyric-title\",\n songArtist: \".lyric-artist a, .lyric-artist\",\n} as const;\n","/**\n * Parser for a lyrics.com song page.\n */\n\nimport { load } from \"cheerio\";\nimport type { CheerioAPI } from \"cheerio\";\nimport { parseFailure } from \"../../errors.js\";\nimport type { SongPage } from \"../../types.js\";\nimport { cleanInlineText, cleanLyricsText } from \"../../text/normalize.js\";\nimport { SEL } from \"./selectors.js\";\n\n/** The wording the site puts in the title of a page holding no words. */\nconst NO_LYRICS_FOUND = /no lyrics found/i;\n\nexport interface ParseSongOptions {\n id: string;\n url: string;\n}\n\nexport function parseSongPage(html: string, options: ParseSongOptions): SongPage {\n const $ = load(html);\n\n const title = cleanInlineText($(SEL.songTitle).first().text()) || null;\n const artist = cleanInlineText($(SEL.songArtist).first().text()) || null;\n\n const body = $(SEL.songBody).first();\n if (body.length === 0) {\n // A valid song page can legitimately carry no lyrics. That is an answer,\n // not a failure, so it is only treated as breakage when the page says\n // nothing about why the text is missing.\n const declaresNoLyrics =\n $(SEL.songNoData).length > 0 || NO_LYRICS_FOUND.test($(\"title\").text());\n if (!declaresNoLyrics) {\n throw parseFailure(options.url, \"no lyrics container and no 'no lyrics' marker\");\n }\n return { id: options.id, url: options.url, title, artist, lyrics: \"\", hasLyrics: false };\n }\n\n const lyrics = cleanLyricsText(extractBodyText($, body));\n if (!lyrics) {\n return { id: options.id, url: options.url, title, artist, lyrics: \"\", hasLyrics: false };\n }\n\n return { id: options.id, url: options.url, title, artist, lyrics, hasLyrics: true };\n}\n\n/**\n * lyrics.com wraps many individual words in links to its dictionary pages.\n * Those anchors are unwrapped in the raw HTML rather than through the DOM,\n * because DOM-level removal loses the line breaks that give lyrics their shape.\n */\nfunction extractBodyText($: CheerioAPI, body: ReturnType<CheerioAPI>): string {\n const rawHtml = $.html(body);\n const withoutAnchors = rawHtml.replace(/<a[^>]*>([\\s\\S]*?)<\\/a>/gi, \"$1\");\n const withNewlines = withoutAnchors.replace(/<br\\s*\\/?>/gi, \"\\n\");\n return load(withNewlines).root().text();\n}\n","/**\n * High-level lyrics.com client.\n *\n * This module knows nothing about MCP, which keeps it unit-testable against\n * plain strings and usable as a plain library through the `./client` export.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport {\n DEFAULT_USER_AGENT,\n MIN_ALLOWED_INTERVAL_MS,\n createLogger,\n loadConfig,\n} from \"../config.js\";\nimport type { SearchPage, SongPage } from \"../types.js\";\nimport { TtlLruCache } from \"./cache.js\";\nimport { fetchHtml } from \"./http.js\";\nimport { parseSearchResults } from \"./parsers/searchResults.js\";\nimport { parseSongPage } from \"./parsers/songPage.js\";\nimport { RateLimiter } from \"./rateLimiter.js\";\nimport { buildSearchUrl, resolveSongRef } from \"./urls.js\";\n\n/** The names a User-Agent carries when it passes traffic off as a browser. */\nconst BROWSER_IDENTITY = /mozilla\\/|applewebkit|chrome\\/|safari\\/|gecko/i;\n\nexport interface LyricsComClientOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\nexport interface FetchOutcome<T> {\n data: T;\n /** True when the page came from the in-memory cache rather than the network. */\n cached: boolean;\n}\n\n/**\n * Apply the guarantees this project makes about its own traffic.\n *\n * The environment parser already enforces both, but `LyricsComClient` is published as a\n * library through the `./client` export and takes a caller-built config, so\n * without this the pacing floor and the honest identity are optional for anyone\n * importing it. lyrics.com answers a burst by refusing, and an honest User-Agent is what it accepts, and those promises hold on every path.\n *\n * A caller may still name their own application in the User-Agent. Passing the\n * traffic off as a browser is a different thing, and gets the project's own\n * identity appended so it stays attributable.\n */\nfunction withGuarantees(config: Config): Config {\n const userAgent = BROWSER_IDENTITY.test(config.userAgent)\n ? `${config.userAgent} ${DEFAULT_USER_AGENT}`\n : config.userAgent;\n return {\n ...config,\n userAgent,\n minIntervalMs: Math.max(MIN_ALLOWED_INTERVAL_MS, config.minIntervalMs),\n };\n}\n\nexport class LyricsComClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly limiter: RateLimiter;\n private readonly cache: TtlLruCache<unknown>;\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: LyricsComClientOptions = {}) {\n this.config = withGuarantees(options.config ?? loadConfig());\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.limiter = new RateLimiter({ minIntervalMs: this.config.minIntervalMs });\n this.cache = new TtlLruCache<unknown>(this.config.cacheMaxEntries, this.config.cacheTtlMs);\n this.fetchImpl = options.fetchImpl;\n }\n\n async search(term: string, page: number): Promise<FetchOutcome<SearchPage>> {\n const url = buildSearchUrl(term, page);\n return await this.fetchParsed(url, (html) => parseSearchResults(html, { page, url }));\n }\n\n async getSong(ref: { id?: string; url?: string }): Promise<FetchOutcome<SongPage>> {\n const { id, url } = resolveSongRef(ref);\n return await this.fetchParsed(url, (html) => parseSongPage(html, { id, url }));\n }\n\n /**\n * Fetch, parse, then cache. In that order: a page that could not be read is\n * never stored, so a bad minute at lyrics.com cannot be replayed from memory for\n * the rest of the cache lifetime, leaving the tool unable to recover after\n * the site comes back.\n *\n * The cached value is the parsed result rather than the raw page, which also\n * keeps a few hundred kilobytes of markup per entry out of memory.\n */\n private async fetchParsed<T>(url: string, parse: (html: string) => T): Promise<FetchOutcome<T>> {\n const hit = this.cache.get(url);\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${url}`);\n return { data: hit as T, cached: true };\n }\n\n const html = await fetchHtml(url, {\n config: this.config,\n limiter: this.limiter,\n logger: this.logger,\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n });\n\n const data = parse(html);\n this.cache.set(url, data);\n return { data, cached: false };\n }\n}\n"],"mappings":"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","names":["process","process","load","load"]}
|
|
1
|
+
{"version":3,"sources":["../../src/config.ts","../../src/version.ts","../../src/lyricscom/cache.ts","../../src/errors.ts","../../src/lyricscom/parsers/detectThrottle.ts","../../src/lyricscom/rateLimiter.ts","../../src/lyricscom/http.ts","../../src/lyricscom/parsers/searchResults.ts","../../src/text/normalize.ts","../../src/lyricscom/urls.ts","../../src/lyricscom/parsers/selectors.ts","../../src/lyricscom/parsers/songPage.ts","../../src/lyricscom/client.ts"],"sourcesContent":["/**\n * Runtime configuration, read from environment variables.\n *\n * A bad value never crashes the process: an MCP server that dies at startup\n * because of a typo in a client config file is very hard to diagnose from the\n * host application, so invalid input is clamped and reported on stderr.\n */\n\nimport process from \"node:process\";\nimport { PKG_VERSION, REPO_URL } from \"./version.js\";\n\nexport type LogLevel = \"silent\" | \"error\" | \"info\" | \"debug\";\n\nexport interface Config {\n userAgent: string;\n minIntervalMs: number;\n timeoutMs: number;\n maxRetries: number;\n cacheTtlMs: number;\n cacheMaxEntries: number;\n logLevel: LogLevel;\n}\n\n/**\n * Identifies the client honestly. lyrics.com serves this fine; it blocks\n * generic tool agents such as `curl/x.y.z`, which is what the override is for.\n */\nexport const DEFAULT_USER_AGENT = `mcp-lyricscom/${PKG_VERSION} (+${REPO_URL})`;\n\nexport const DEFAULTS = {\n minIntervalMs: 1100,\n timeoutMs: 15_000,\n maxRetries: 3,\n cacheTtlMs: 15 * 60 * 1000,\n cacheMaxEntries: 200,\n logLevel: \"error\" as LogLevel,\n};\n\n/**\n * Floor on the request interval, enforced regardless of configuration.\n *\n * The pacing is what keeps this client from behaving like a crawler against a\n * site that has no API and no published rate limit. Leaving it configurable down\n * to zero would make the politeness of every installation depend on whoever\n * edited a JSON file, so a value below this floor is refused and the default is\n * used instead.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 500;\n\nconst LOG_LEVELS: LogLevel[] = [\"silent\", \"error\", \"info\", \"debug\"];\n\ninterface NumericRange {\n min: number;\n max: number;\n fallback: number;\n}\n\nfunction readNumber(name: string, env: NodeJS.ProcessEnv, range: NumericRange): number {\n const raw = env[name];\n if (raw === undefined || raw.trim() === \"\") {\n return range.fallback;\n }\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`${name}=\"${raw}\" is not a number, using ${range.fallback}`);\n return range.fallback;\n }\n const clamped = Math.min(range.max, Math.max(range.min, Math.round(parsed)));\n if (clamped !== Math.round(parsed)) {\n warn(`${name}=${raw} is out of range, clamped to ${clamped}`);\n }\n return clamped;\n}\n\nfunction warn(message: string): void {\n process.stderr.write(`[mcp-lyricscom] ${message}\\n`);\n}\n\n/**\n * Read the request interval, refusing anything below the floor.\n *\n * A value under the floor falls back to the default rather than to the floor\n * itself: someone who set 0 was not asking for 500, they were asking for no\n * pacing at all, and the safe reading of that request is to ignore it.\n */\nfunction readInterval(env: NodeJS.ProcessEnv): number {\n const raw = env.LYRICSCOM_MIN_INTERVAL_MS;\n if (raw === undefined || raw.trim() === \"\") {\n return DEFAULTS.minIntervalMs;\n }\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`LYRICSCOM_MIN_INTERVAL_MS=\"${raw}\" is not a number, using ${DEFAULTS.minIntervalMs}ms`);\n return DEFAULTS.minIntervalMs;\n }\n\n const rounded = Math.round(parsed);\n if (rounded < MIN_ALLOWED_INTERVAL_MS) {\n warn(\n `LYRICSCOM_MIN_INTERVAL_MS=${raw} is below the ${MIN_ALLOWED_INTERVAL_MS}ms floor and was ignored; ` +\n `using ${DEFAULTS.minIntervalMs}ms. This floor keeps the client from hammering lyrics.com.`,\n );\n return DEFAULTS.minIntervalMs;\n }\n\n return Math.min(60_000, rounded);\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const rawUserAgent = env.LYRICSCOM_USER_AGENT?.trim();\n const rawLogLevel = env.LYRICSCOM_LOG_LEVEL?.trim().toLowerCase();\n\n let logLevel = DEFAULTS.logLevel;\n if (rawLogLevel) {\n if (LOG_LEVELS.includes(rawLogLevel as LogLevel)) {\n logLevel = rawLogLevel as LogLevel;\n } else {\n warn(`LYRICSCOM_LOG_LEVEL=\"${rawLogLevel}\" is unknown, using \"${DEFAULTS.logLevel}\"`);\n }\n }\n\n return {\n userAgent: rawUserAgent || DEFAULT_USER_AGENT,\n minIntervalMs: readInterval(env),\n timeoutMs: readNumber(\"LYRICSCOM_TIMEOUT_MS\", env, {\n min: 1000,\n max: 120_000,\n fallback: DEFAULTS.timeoutMs,\n }),\n maxRetries: readNumber(\"LYRICSCOM_MAX_RETRIES\", env, {\n min: 0,\n max: 10,\n fallback: DEFAULTS.maxRetries,\n }),\n cacheTtlMs: readNumber(\"LYRICSCOM_CACHE_TTL_MS\", env, {\n min: 0,\n max: 24 * 60 * 60 * 1000,\n fallback: DEFAULTS.cacheTtlMs,\n }),\n cacheMaxEntries: readNumber(\"LYRICSCOM_CACHE_MAX_ENTRIES\", env, {\n min: 0,\n max: 10_000,\n fallback: DEFAULTS.cacheMaxEntries,\n }),\n logLevel,\n };\n}\n\nconst LEVEL_RANK: Record<LogLevel, number> = { silent: 0, error: 1, info: 2, debug: 3 };\n\n/**\n * Logs go to stderr without exception. On a stdio transport, stdout carries the\n * protocol and any stray write there corrupts the session.\n */\nexport function createLogger(level: LogLevel) {\n const emit = (at: LogLevel, message: string) => {\n if (LEVEL_RANK[level] >= LEVEL_RANK[at]) {\n process.stderr.write(`[mcp-lyricscom] ${message}\\n`);\n }\n };\n return {\n error: (message: string) => emit(\"error\", message),\n info: (message: string) => emit(\"info\", message),\n debug: (message: string) => emit(\"debug\", message),\n };\n}\n\nexport type Logger = ReturnType<typeof createLogger>;\n","/**\n * Kept in sync with package.json. The User-Agent embeds it, so it must not\n * drift; the release checklist bumps both together.\n */\nexport const PKG_VERSION = \"2.1.0\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-lyricscom\";\n","/**\n * In-memory TTL + LRU cache.\n *\n * Nothing is written to disk. The cache exists so that repeated questions about\n * the same song inside one conversation do not hit lyrics.com again, not to\n * build a local copy of the site.\n *\n * A Map iterates in insertion order, so re-inserting on every hit is enough to\n * make the first key the least recently used one.\n */\n\ninterface Entry<V> {\n value: V;\n expiresAt: number;\n}\n\nexport class TtlLruCache<V> {\n private readonly store = new Map<string, Entry<V>>();\n\n constructor(\n private readonly maxEntries: number,\n private readonly ttlMs: number,\n ) {}\n\n get(key: string): V | undefined {\n const entry = this.store.get(key);\n if (!entry) {\n return undefined;\n }\n if (Date.now() >= entry.expiresAt) {\n this.store.delete(key);\n return undefined;\n }\n this.store.delete(key);\n this.store.set(key, entry);\n return entry.value;\n }\n\n set(key: string, value: V): void {\n if (this.maxEntries <= 0 || this.ttlMs <= 0) {\n return;\n }\n this.store.delete(key);\n this.store.set(key, { value, expiresAt: Date.now() + this.ttlMs });\n while (this.store.size > this.maxEntries) {\n const oldest = this.store.keys().next();\n if (oldest.done) {\n break;\n }\n this.store.delete(oldest.value);\n }\n }\n\n get size(): number {\n return this.store.size;\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","/**\n * Error taxonomy surfaced to the calling model.\n *\n * The point of this file is that an LLM must be able to tell \"lyrics.com pushed\n * back on me\" apart from \"there is no such song\". The reference implementation\n * this server replaces collapsed every failure into an empty list, which made\n * rate limiting look like an absence of results.\n */\n\nexport type ErrorCode =\n | \"throttled\"\n | \"blocked_user_agent\"\n | \"not_found\"\n | \"invalid_input\"\n | \"parse_failure\"\n | \"network_error\"\n | \"timeout\";\n\nexport interface ErrorDetails {\n url?: string;\n status?: number;\n retryAfterMs?: number;\n hint?: string;\n}\n\nexport class LyricsComError extends Error {\n constructor(\n readonly code: ErrorCode,\n message: string,\n readonly details: ErrorDetails = {},\n ) {\n super(message);\n this.name = \"LyricsComError\";\n }\n}\n\nconst ISSUES_URL = \"https://github.com/smeet666/mcp-lyricscom/issues\";\n\nexport function throttled(url: string, retryAfterMs: number): LyricsComError {\n return new LyricsComError(\n \"throttled\",\n \"lyrics.com is rate limiting this client. It answered with an empty response instead of a page. \" +\n \"This does NOT mean there are no results for this query.\",\n {\n url,\n retryAfterMs,\n hint:\n \"Wait at least a minute before calling the same tool again with the same arguments. Once lyrics.com \" +\n \"starts throttling, the window commonly lasts several minutes, so retrying immediately will fail again. \" +\n \"If it keeps happening, raise LYRICSCOM_MIN_INTERVAL_MS in your MCP client configuration.\",\n },\n );\n}\n\nexport function blockedUserAgent(url: string): LyricsComError {\n return new LyricsComError(\n \"blocked_user_agent\",\n \"lyrics.com refused the request because of the User-Agent this server sends (HTTP 403).\",\n {\n url,\n status: 403,\n hint:\n \"Set the LYRICSCOM_USER_AGENT environment variable in your MCP client configuration to a different \" +\n \"value. See the Configuration section of the project README.\",\n },\n );\n}\n\nexport function notFound(url: string): LyricsComError {\n return new LyricsComError(\"not_found\", \"lyrics.com has no page at this address.\", {\n url,\n status: 404,\n hint: \"Check the song id. Ids come from the search tools; they are not stable to guess by hand.\",\n });\n}\n\nexport function parseFailure(url: string, what: string): LyricsComError {\n return new LyricsComError(\n \"parse_failure\",\n `The page loaded but the expected lyrics.com markup was not found (${what}). The site layout may have changed.`,\n { url, hint: `Please report this, with the query you used, at ${ISSUES_URL}` },\n );\n}\n\nexport function invalidInput(message: string, hint?: string): LyricsComError {\n return new LyricsComError(\"invalid_input\", message, hint ? { hint } : {});\n}\n","/**\n * Response classification.\n *\n * lyrics.com does not answer rate limiting with 429. It answers with HTTP 202\n * and a zero-byte body, which a naive client reads as a successful but empty\n * page. Every response therefore goes through this classifier before any\n * parsing happens.\n */\n\nconst CLOSING_HTML_TAG = /<\\/html>/i;\n\nexport type ResponseVerdict =\n | { kind: \"ok\"; html: string }\n | { kind: \"throttled\"; reason: ThrottleReason }\n | { kind: \"blocked\"; status: number }\n | { kind: \"not-found\" }\n | { kind: \"server-error\"; status: number };\n\nexport type ThrottleReason =\n | \"status-202\"\n | \"status-429\"\n | \"status-503\"\n | \"empty-body\"\n | \"short-body\";\n\n/**\n * A real search page weighs about 187 KB and a song page 60 KB or more, so a\n * body this small is never a genuine lyrics.com page.\n */\nexport const MIN_PLAUSIBLE_HTML = 2000;\n\nexport function classifyResponse(status: number, body: string): ResponseVerdict {\n if (status === 202) {\n return { kind: \"throttled\", reason: \"status-202\" };\n }\n if (status === 429) {\n return { kind: \"throttled\", reason: \"status-429\" };\n }\n if (status === 503) {\n return { kind: \"throttled\", reason: \"status-503\" };\n }\n if (status === 403) {\n return { kind: \"blocked\", status };\n }\n if (status === 404) {\n return { kind: \"not-found\" };\n }\n if (status >= 500) {\n return { kind: \"server-error\", status };\n }\n\n const trimmed = body.trim();\n if (trimmed.length === 0) {\n return { kind: \"throttled\", reason: \"empty-body\" };\n }\n if (trimmed.length < MIN_PLAUSIBLE_HTML && !CLOSING_HTML_TAG.test(trimmed)) {\n return { kind: \"throttled\", reason: \"short-body\" };\n }\n\n return { kind: \"ok\", html: body };\n}\n","/**\n * Serial request queue with an adaptive minimum interval.\n *\n * lyrics.com tolerates a steady trickle but stops answering after a short burst,\n * so requests are run one at a time with a floor on the gap between starts.\n * When the site does push back, the interval doubles and then decays back down\n * as requests succeed, which recovers faster than a fixed delay and behaves\n * better than hammering at a constant rate.\n */\n\nexport interface RateLimiterOptions {\n minIntervalMs: number;\n maxIntervalMs?: number;\n}\n\nconst DEFAULT_MAX_INTERVAL_MS = 10_000;\n\nexport class RateLimiter {\n private readonly baseIntervalMs: number;\n private readonly maxIntervalMs: number;\n private intervalMs: number;\n /** Tail of the queue: each task chains onto the previous one. */\n private tail: Promise<unknown> = Promise.resolve();\n private lastStart = 0;\n\n constructor(options: RateLimiterOptions) {\n this.baseIntervalMs = Math.max(0, options.minIntervalMs);\n this.maxIntervalMs = Math.max(\n this.baseIntervalMs,\n options.maxIntervalMs ?? DEFAULT_MAX_INTERVAL_MS,\n );\n this.intervalMs = this.baseIntervalMs;\n }\n\n get currentIntervalMs(): number {\n return this.intervalMs;\n }\n\n /** Queue a task. Tasks run in call order, one at a time. */\n schedule<T>(task: () => Promise<T>): Promise<T> {\n const run = this.tail.then(async () => task());\n // The queue must keep draining even when a task rejects.\n this.tail = run.then(\n () => undefined,\n () => undefined,\n );\n return run;\n }\n\n /**\n * Wait for this request's slot, then claim it.\n *\n * Called once per upstream request rather than once per task, because a task\n * runs a whole retry chain: stamping only its start would let the next task\n * follow the chain's last request with no gap at all.\n */\n async beforeRequest(): Promise<void> {\n await this.waitForSlot();\n this.lastStart = Date.now();\n }\n\n /** Called after the site signals it is under load. */\n penalize(): void {\n const next = this.intervalMs === 0 ? 250 : this.intervalMs * 2;\n this.intervalMs = Math.min(this.maxIntervalMs, next);\n }\n\n /** Called after a success, so a single hiccup does not slow things down forever. */\n relax(): void {\n this.intervalMs = Math.max(this.baseIntervalMs, Math.floor(this.intervalMs * 0.75));\n }\n\n private async waitForSlot(): Promise<void> {\n if (this.intervalMs === 0 || this.lastStart === 0) {\n return;\n }\n const elapsed = Date.now() - this.lastStart;\n // Clamped to the interval: a clock stepped backwards, by NTP or a resumed\n // virtual machine, would otherwise make this wait for the size of the step,\n // and the queue is serial so every pending request would wait behind it.\n const remaining = Math.min(this.intervalMs, this.intervalMs - elapsed);\n if (remaining > 0) {\n await sleep(remaining);\n }\n }\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * HTTP layer: one GET, classified, with backoff.\n *\n * The retry policy is domain specific rather than generic, because the failure\n * this server exists to handle does not look like a failure at the transport\n * level: lyrics.com answers a throttled request with HTTP 202 and an empty\n * body. Only `classifyResponse` can tell that apart from a real page.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport { blockedUserAgent, LyricsComError, notFound, throttled } from \"../errors.js\";\nimport { classifyResponse } from \"./parsers/detectThrottle.js\";\nimport { type RateLimiter, sleep } from \"./rateLimiter.js\";\n\nconst BACKOFF_BASE_MS = 2000;\nconst BACKOFF_FACTOR = 2;\nconst BACKOFF_MAX_MS = 20_000;\n\n/** Exponential backoff with jitter, so parallel clients do not resynchronise. */\nexport function backoffDelay(attempt: number, random: () => number = Math.random): number {\n const uncapped = BACKOFF_BASE_MS * BACKOFF_FACTOR ** attempt;\n const capped = Math.min(BACKOFF_MAX_MS, uncapped);\n return Math.round(capped * (0.5 + random() * 0.5));\n}\n\nexport interface HttpDeps {\n config: Config;\n limiter: RateLimiter;\n logger: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/**\n * Fetch one page as HTML, retrying transient conditions.\n *\n * The whole retry loop, backoff sleeps included, runs inside a single limiter\n * slot. Waiting outside it would let a queued request slip into the same\n * throttling window that the current one is backing away from.\n */\nexport async function fetchHtml(url: string, deps: HttpDeps): Promise<string> {\n const { config, limiter, logger } = deps;\n const doFetch = deps.fetchImpl ?? fetch;\n\n return await limiter.schedule(async () => {\n let lastError: LyricsComError | undefined;\n\n // Set when the site says how long to stay away; it replaces our own guess\n // for the next attempt. Applied here rather than where it is read, so no\n // wait is ever served after the last attempt, when nobody would use it.\n let askedWaitMs: number | null = null;\n\n for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {\n if (attempt > 0) {\n const delay = Math.min(askedWaitMs ?? backoffDelay(attempt - 1), BACKOFF_MAX_MS);\n askedWaitMs = null;\n logger.info(`retry ${attempt}/${config.maxRetries} in ${delay}ms for ${url}`);\n await sleep(delay);\n }\n\n let status: number;\n let body: string;\n let retryAfterMs: number | null = null;\n try {\n await limiter.beforeRequest();\n const response = await doFetch(url, {\n headers: {\n \"User-Agent\": config.userAgent,\n Accept: \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n \"Accept-Language\": \"en-US,en;q=0.9,fr;q=0.8\",\n },\n redirect: \"follow\",\n signal: AbortSignal.timeout(config.timeoutMs),\n });\n status = response.status;\n retryAfterMs = parseRetryAfter(response.headers.get(\"retry-after\"));\n body = await response.text();\n } catch (error) {\n lastError = asTransportError(error, url);\n logger.debug(`${lastError.code} for ${url}: ${lastError.message}`);\n continue;\n }\n\n const verdict = classifyResponse(status, body);\n switch (verdict.kind) {\n case \"ok\":\n limiter.relax();\n return verdict.html;\n\n case \"throttled\":\n limiter.penalize();\n logger.info(\n `throttled (${verdict.reason}) on ${url}, interval now ${limiter.currentIntervalMs}ms`,\n );\n // A server that says when to come back knows better than our own guess.\n askedWaitMs = retryAfterMs;\n lastError = throttled(url, retryAfterMs ?? backoffDelay(attempt));\n continue;\n\n case \"server-error\":\n lastError = new LyricsComError(\n \"network_error\",\n `lyrics.com returned HTTP ${verdict.status}.`,\n {\n url,\n status: verdict.status,\n },\n );\n continue;\n\n // Retrying these would only repeat the same answer.\n case \"blocked\":\n throw blockedUserAgent(url);\n case \"not-found\":\n throw notFound(url);\n\n // Every kind the classifier declares is handled above, and the linter\n // holds that. This clause is the runtime backstop: a response nothing\n // above matched leaves as an error rather than as a silent retry.\n default:\n throw new LyricsComError(\n \"network_error\",\n `lyrics.com returned a response this client cannot classify for ${url}.`,\n { url },\n );\n }\n }\n\n throw lastError ?? new LyricsComError(\"network_error\", `Could not fetch ${url}.`, { url });\n });\n}\n\n/** `Retry-After` carries either seconds or an HTTP date. */\nfunction parseRetryAfter(raw: string | null): number | null {\n if (!raw) {\n return null;\n }\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) {\n return Math.round(seconds * 1000);\n }\n const when = Date.parse(raw);\n if (Number.isNaN(when)) {\n return null;\n }\n return Math.max(0, when - Date.now());\n}\n\nfunction asTransportError(error: unknown, url: string): LyricsComError {\n if (error instanceof LyricsComError) {\n return error;\n }\n const name = error instanceof Error ? error.name : \"\";\n if (name === \"TimeoutError\" || name === \"AbortError\") {\n return new LyricsComError(\"timeout\", \"lyrics.com did not answer in time.\", {\n url,\n hint: \"Raise LYRICSCOM_TIMEOUT_MS if this happens often on a slow connection.\",\n });\n }\n const message = error instanceof Error ? error.message : String(error);\n return new LyricsComError(\"network_error\", `Could not reach lyrics.com: ${message}`, { url });\n}\n","/**\n * Parser for a lyrics.com search results page.\n *\n * Page 1 (`/lyrics/{term}`) and later pages (`/serp.php?st=...&p=N`) share the\n * same markup, so one parser covers both.\n */\n\nimport process from \"node:process\";\nimport { load } from \"cheerio\";\nimport { parseFailure } from \"../../errors.js\";\nimport type { SearchPage, SongResult } from \"../../types.js\";\nimport { cleanInlineText, cleanLyricsText } from \"../../text/normalize.js\";\nimport { extractSongId, RESULTS_PER_PAGE, toAbsoluteUrl } from \"../urls.js\";\nimport { SEL } from \"./selectors.js\";\n\n/** A year a record can plausibly carry, from the 1800s to this century. */\nconst YEAR = /\\b(1[89]\\d{2}|20\\d{2})\\b/;\n\nexport interface ParseSearchOptions {\n page: number;\n /** Reported in errors so a failure points at the request that caused it. */\n url: string;\n}\n\nexport function parseSearchResults(html: string, options: ParseSearchOptions): SearchPage {\n const $ = load(html);\n const rows = $(SEL.serpItem);\n\n if (rows.length === 0) {\n // An empty result set still renders the page chrome. Its absence means the\n // response was not the page we asked for, which must not be reported as\n // \"no songs found\".\n if ($(SEL.serpShell).length === 0) {\n throw parseFailure(options.url, \"no result rows and no recognizable search page structure\");\n }\n return { results: [], page: options.page, rawCount: 0, hasMore: false };\n }\n\n const results: SongResult[] = [];\n let skipped = 0;\n\n rows.each((_, element) => {\n const row = $(element);\n const titleLink = row.find(SEL.resultTitle).first();\n const href = titleLink.attr(\"href\");\n const title = cleanInlineText(titleLink.text());\n const artist = cleanInlineText(row.find(SEL.resultArtist).first().text());\n\n const id = href ? extractSongId(href) : null;\n if (!(id && title && artist)) {\n skipped += 1;\n return;\n }\n\n const album = cleanInlineText(row.find(SEL.resultAlbum).first().text()) || null;\n const year = parseYear(row.find(SEL.resultYear).first().text());\n const snippetRaw = row.find(SEL.resultSnippet).first().text();\n const snippet = snippetRaw ? cleanLyricsText(snippetRaw) || null : null;\n\n results.push({\n id,\n url: toAbsoluteUrl(href as string),\n title,\n artist,\n album,\n year,\n snippet,\n });\n });\n\n if (results.length === 0) {\n throw parseFailure(options.url, `${rows.length} result rows matched but none could be read`);\n }\n if (skipped > 0) {\n process.stderr.write(\n `[mcp-lyricscom] skipped ${skipped} unreadable result rows on ${options.url}\\n`,\n );\n }\n\n return {\n results,\n page: options.page,\n rawCount: rows.length,\n hasMore: rows.length >= RESULTS_PER_PAGE,\n };\n}\n\n/** Year cells look like \"1971\" but are sometimes decorated or empty. */\nfunction parseYear(raw: string): number | null {\n const match = YEAR.exec(raw);\n if (!match?.[1]) {\n return null;\n }\n return Number.parseInt(match[1], 10);\n}\n","/**\n * Text cleaning for scraped content: HTML entities, mojibake, whitespace.\n */\n\n/** The letters UTF-8 read as Latin-1 leaves behind. */\nconst MOJIBAKE_MARKS = /[ÃÂ]/;\n\nconst HTML_ENTITIES: Record<string, string> = {\n amp: \"&\",\n lt: \"<\",\n gt: \">\",\n quot: '\"',\n apos: \"'\",\n nbsp: \" \",\n hellip: \"…\",\n mdash: \"—\",\n ndash: \"–\",\n rsquo: \"’\",\n lsquo: \"‘\",\n ldquo: \"“\",\n rdquo: \"”\",\n};\n\n/** Decode named and numeric HTML entities. */\nexport function unescapeHtml(text: string): string {\n if (!text) {\n return text;\n }\n return text.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (match, entity: string) => {\n if (entity.startsWith(\"#\")) {\n const isHex = entity[1] === \"x\" || entity[1] === \"X\";\n const codePoint = Number.parseInt(isHex ? entity.slice(2) : entity.slice(1), isHex ? 16 : 10);\n if (Number.isNaN(codePoint) || codePoint < 0 || codePoint > 0x10ffff) {\n return match;\n }\n try {\n return String.fromCodePoint(codePoint);\n } catch {\n return match;\n }\n }\n return HTML_ENTITIES[entity.toLowerCase()] ?? match;\n });\n}\n\n/**\n * Repair UTF-8 bytes that were decoded as Latin-1, which turns \"é\" into \"é\".\n * lyrics.com serves French titles that occasionally arrive in this state.\n * Re-encoding through latin1 recovers the original text; the round trip is only\n * kept when it removes replacement characters, so correct text is never touched.\n */\nexport function fixEncodingIssues(text: string): string {\n if (!(text && MOJIBAKE_MARKS.test(text))) {\n return text;\n }\n try {\n const repaired = Buffer.from(text, \"latin1\").toString(\"utf8\");\n if (repaired.includes(\"�\")) {\n return text;\n }\n return repaired;\n } catch {\n return text;\n }\n}\n\n/** Collapse runs of whitespace and trim. */\nexport function collapseWhitespace(text: string): string {\n return text.replace(/\\s+/g, \" \").trim();\n}\n\n/** Full cleaning pass for a scraped title or artist name. */\nexport function cleanInlineText(text: string): string {\n if (!text) {\n return \"\";\n }\n return collapseWhitespace(fixEncodingIssues(unescapeHtml(text)));\n}\n\n/**\n * Normalize a lyrics block: trim each line, drop leading and trailing blank\n * lines, and collapse runs of blank lines to a single verse separator.\n */\nexport function cleanLyricsText(text: string): string {\n if (!text) {\n return \"\";\n }\n const normalized = fixEncodingIssues(unescapeHtml(text)).replace(/\\r\\n?/g, \"\\n\");\n const lines = normalized.split(\"\\n\").map((line) => line.trim());\n\n const out: string[] = [];\n for (const line of lines) {\n if (line === \"\" && (out.length === 0 || out.at(-1) === \"\")) {\n continue;\n }\n out.push(line);\n }\n while (out.length > 0 && out.at(-1) === \"\") {\n out.pop();\n }\n return out.join(\"\\n\");\n}\n","/**\n * URL construction and validation for lyrics.com.\n */\n\nimport { invalidInput } from \"../errors.js\";\n\nconst ABSOLUTE_URL = /^https?:\\/\\//i;\nconst DIGITS_ONLY = /^\\d+$/;\n\nexport const BASE_URL = \"https://www.lyrics.com\";\n\n/** lyrics.com renders 24 rows per search page. */\nexport const RESULTS_PER_PAGE = 24;\n\n/**\n * Song page paths come in two shapes: the current `/lyric-lf/{id}/...` and the\n * older `/lyric/{id}/...` still present in archived links.\n */\nexport const SONG_HREF_RE = /^\\/lyric(?:-lf)?\\/(\\d+)\\//;\n\nconst ALLOWED_HOSTS = new Set([\"lyrics.com\", \"www.lyrics.com\"]);\n\n/**\n * Percent-encode a search term the way lyrics.com expects, with spaces as `+`.\n */\nfunction encodeTerm(term: string): string {\n return encodeURIComponent(term).replace(/%20/g, \"+\");\n}\n\n/**\n * Page 1 lives at a path, later pages at a query string. Both return the same\n * markup, so the parser does not need to know which one produced it.\n */\nexport function buildSearchUrl(term: string, page: number): string {\n const encoded = encodeTerm(term);\n if (page <= 1) {\n return `${BASE_URL}/lyrics/${encoded}`;\n }\n return `${BASE_URL}/serp.php?st=${encoded}&p=${page}`;\n}\n\nexport function buildSongUrl(id: string): string {\n return `${BASE_URL}/lyric-lf/${id}/`;\n}\n\n/** True only for lyrics.com itself, so a hostile URL cannot be used as a proxy. */\nexport function isLyricsComHost(rawUrl: string): boolean {\n let parsed: URL;\n try {\n parsed = new URL(rawUrl);\n } catch {\n return false;\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n return false;\n }\n return ALLOWED_HOSTS.has(parsed.hostname.toLowerCase());\n}\n\n/** Extract the numeric song id from a lyrics.com song path or absolute URL. */\nexport function extractSongId(hrefOrUrl: string): string | null {\n let path = hrefOrUrl;\n if (ABSOLUTE_URL.test(hrefOrUrl)) {\n if (!isLyricsComHost(hrefOrUrl)) {\n return null;\n }\n path = new URL(hrefOrUrl).pathname;\n }\n if (!path.startsWith(\"/\")) {\n path = `/${path}`;\n }\n const match = SONG_HREF_RE.exec(path);\n return match?.[1] ?? null;\n}\n\n/**\n * Resolve the `id` / `url` pair accepted by get_lyrics into a canonical\n * request URL. `id` wins when both are given.\n */\nexport function resolveSongRef(input: { id?: string; url?: string }): { id: string; url: string } {\n if (input.id) {\n if (!DIGITS_ONLY.test(input.id)) {\n throw invalidInput(\n `\"${input.id}\" is not a lyrics.com song id.`,\n \"Ids are digits only, as returned by the search tools.\",\n );\n }\n return { id: input.id, url: buildSongUrl(input.id) };\n }\n\n if (input.url) {\n if (!isLyricsComHost(input.url)) {\n throw invalidInput(\n \"Only www.lyrics.com URLs are accepted.\",\n \"Pass the source_url returned by search_lyrics or search_songs, or use the numeric id instead.\",\n );\n }\n const id = extractSongId(input.url);\n if (!id) {\n throw invalidInput(\n \"That lyrics.com URL is not a song page.\",\n \"Song pages look like https://www.lyrics.com/lyric-lf/1234567/Artist/Title\",\n );\n }\n return { id, url: input.url };\n }\n\n throw invalidInput(\"Either 'id' or 'url' must be provided.\");\n}\n\n/** Absolute URL for a href scraped out of a result row. */\nexport function toAbsoluteUrl(href: string): string {\n return new URL(href, BASE_URL).toString();\n}\n","/**\n * Every lyrics.com CSS selector lives here, so a site redesign is a one-file fix.\n *\n * Selectors deliberately target a single stable class rather than a full class\n * list (`div.sec-lyric`, not `div.sec-lyric.clearfix`), so that a layout tweak\n * adding or removing a utility class does not break extraction.\n */\n\nexport const SEL = {\n /** One search result row. */\n serpItem: \"div.sec-lyric\",\n /**\n * Page chrome that is present whether or not there are results. Used to tell\n * \"zero results\" apart from \"the markup changed\", which must not look alike.\n */\n serpShell: \"#lyric-search, .lyric-search, .pagination, #main-content, form[action*='serp']\",\n resultTitle: \"p.lyric-meta-title a\",\n /**\n * Rows tied to an album expose the artist under `lyric-meta-album-artist`,\n * rows without one under `lyric-meta-artists`. Both shapes appear on the same\n * page, so missing the second silently drops results.\n */\n resultArtist: \"p.lyric-meta-album-artist a, p.lyric-meta-artists a\",\n resultAlbum: \"p.lyric-meta-album\",\n resultYear: \"p.lyric-meta-album-year\",\n resultSnippet: \"pre.lyric-body\",\n\n /** Full lyrics on a song page, with the historical fallback. */\n songBody: \"pre#lyric-body-text, pre.lyric-body\",\n /** Marker for a valid song page that simply has no lyrics on file. */\n songNoData: \"div.lyric-no-data\",\n songTitle: \"#lyric-title-text, .lyric-title\",\n songArtist: \".lyric-artist a, .lyric-artist\",\n} as const;\n","/**\n * Parser for a lyrics.com song page.\n */\n\nimport { load } from \"cheerio\";\nimport type { CheerioAPI } from \"cheerio\";\nimport { parseFailure } from \"../../errors.js\";\nimport type { SongPage } from \"../../types.js\";\nimport { cleanInlineText, cleanLyricsText } from \"../../text/normalize.js\";\nimport { SEL } from \"./selectors.js\";\n\n/** The wording the site puts in the title of a page holding no words. */\nconst NO_LYRICS_FOUND = /no lyrics found/i;\n\nexport interface ParseSongOptions {\n id: string;\n url: string;\n}\n\nexport function parseSongPage(html: string, options: ParseSongOptions): SongPage {\n const $ = load(html);\n\n const title = cleanInlineText($(SEL.songTitle).first().text()) || null;\n const artist = cleanInlineText($(SEL.songArtist).first().text()) || null;\n\n const body = $(SEL.songBody).first();\n if (body.length === 0) {\n // A valid song page can legitimately carry no lyrics. That is an answer,\n // not a failure, so it is only treated as breakage when the page says\n // nothing about why the text is missing.\n const declaresNoLyrics =\n $(SEL.songNoData).length > 0 || NO_LYRICS_FOUND.test($(\"title\").text());\n if (!declaresNoLyrics) {\n throw parseFailure(options.url, \"no lyrics container and no 'no lyrics' marker\");\n }\n return { id: options.id, url: options.url, title, artist, lyrics: \"\", hasLyrics: false };\n }\n\n const lyrics = cleanLyricsText(extractBodyText($, body));\n if (!lyrics) {\n return { id: options.id, url: options.url, title, artist, lyrics: \"\", hasLyrics: false };\n }\n\n return { id: options.id, url: options.url, title, artist, lyrics, hasLyrics: true };\n}\n\n/**\n * lyrics.com wraps many individual words in links to its dictionary pages.\n * Those anchors are unwrapped in the raw HTML rather than through the DOM,\n * because DOM-level removal loses the line breaks that give lyrics their shape.\n */\nfunction extractBodyText($: CheerioAPI, body: ReturnType<CheerioAPI>): string {\n const rawHtml = $.html(body);\n const withoutAnchors = rawHtml.replace(/<a[^>]*>([\\s\\S]*?)<\\/a>/gi, \"$1\");\n const withNewlines = withoutAnchors.replace(/<br\\s*\\/?>/gi, \"\\n\");\n return load(withNewlines).root().text();\n}\n","/**\n * High-level lyrics.com client.\n *\n * This module knows nothing about MCP, which keeps it unit-testable against\n * plain strings and usable as a plain library through the `./client` export.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport {\n DEFAULT_USER_AGENT,\n MIN_ALLOWED_INTERVAL_MS,\n createLogger,\n loadConfig,\n} from \"../config.js\";\nimport type { SearchPage, SongPage } from \"../types.js\";\nimport { TtlLruCache } from \"./cache.js\";\nimport { fetchHtml } from \"./http.js\";\nimport { parseSearchResults } from \"./parsers/searchResults.js\";\nimport { parseSongPage } from \"./parsers/songPage.js\";\nimport { RateLimiter } from \"./rateLimiter.js\";\nimport { buildSearchUrl, resolveSongRef } from \"./urls.js\";\n\n/** The names a User-Agent carries when it passes traffic off as a browser. */\nconst BROWSER_IDENTITY = /mozilla\\/|applewebkit|chrome\\/|safari\\/|gecko/i;\n\nexport interface LyricsComClientOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\nexport interface FetchOutcome<T> {\n data: T;\n /** True when the page came from the in-memory cache rather than the network. */\n cached: boolean;\n}\n\n/**\n * Apply the guarantees this project makes about its own traffic.\n *\n * The environment parser already enforces both, but `LyricsComClient` is published as a\n * library through the `./client` export and takes a caller-built config, so\n * without this the pacing floor and the honest identity are optional for anyone\n * importing it. lyrics.com answers a burst by refusing, and an honest User-Agent is what it accepts, and those promises hold on every path.\n *\n * A caller may still name their own application in the User-Agent. Passing the\n * traffic off as a browser is a different thing, and gets the project's own\n * identity appended so it stays attributable.\n */\nfunction withGuarantees(config: Config): Config {\n const userAgent = BROWSER_IDENTITY.test(config.userAgent)\n ? `${config.userAgent} ${DEFAULT_USER_AGENT}`\n : config.userAgent;\n return {\n ...config,\n userAgent,\n minIntervalMs: Math.max(MIN_ALLOWED_INTERVAL_MS, config.minIntervalMs),\n };\n}\n\nexport class LyricsComClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly limiter: RateLimiter;\n private readonly cache: TtlLruCache<unknown>;\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: LyricsComClientOptions = {}) {\n this.config = withGuarantees(options.config ?? loadConfig());\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.limiter = new RateLimiter({ minIntervalMs: this.config.minIntervalMs });\n this.cache = new TtlLruCache<unknown>(this.config.cacheMaxEntries, this.config.cacheTtlMs);\n this.fetchImpl = options.fetchImpl;\n }\n\n async search(term: string, page: number): Promise<FetchOutcome<SearchPage>> {\n const url = buildSearchUrl(term, page);\n return await this.fetchParsed(url, (html) => parseSearchResults(html, { page, url }));\n }\n\n async getSong(ref: { id?: string; url?: string }): Promise<FetchOutcome<SongPage>> {\n const { id, url } = resolveSongRef(ref);\n return await this.fetchParsed(url, (html) => parseSongPage(html, { id, url }));\n }\n\n /**\n * Fetch, parse, then cache. In that order: a page that could not be read is\n * never stored, so a bad minute at lyrics.com cannot be replayed from memory for\n * the rest of the cache lifetime, leaving the tool unable to recover after\n * the site comes back.\n *\n * The cached value is the parsed result rather than the raw page, which also\n * keeps a few hundred kilobytes of markup per entry out of memory.\n */\n private async fetchParsed<T>(url: string, parse: (html: string) => T): Promise<FetchOutcome<T>> {\n const hit = this.cache.get(url);\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${url}`);\n return { data: hit as T, cached: true };\n }\n\n const html = await fetchHtml(url, {\n config: this.config,\n limiter: this.limiter,\n logger: this.logger,\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n });\n\n const data = parse(html);\n this.cache.set(url, data);\n return { data, cached: false };\n }\n}\n"],"mappings":"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","names":["process","process","load","load"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-lyricscom",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "MCP server for lyrics.com. Search song lyrics by word or by title, and fetch full lyrics. No API key required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"server.json"
|
|
44
44
|
],
|
|
45
45
|
"engines": {
|
|
46
|
-
"node": ">=24.
|
|
46
|
+
"node": ">=24.22.1"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "tsup",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"check": "biome ci && prettier --check \"**/*.{md,yml}\"",
|
|
58
58
|
"prepublishOnly": "npm run typecheck && npm run test && npm run build",
|
|
59
59
|
"start": "node dist/index.js",
|
|
60
|
-
"inspector": "npx @modelcontextprotocol/inspector node dist/index.js"
|
|
60
|
+
"inspector": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
61
|
+
"audit:floors": "node scripts/audit-floors.mjs"
|
|
61
62
|
},
|
|
62
63
|
"keywords": [
|
|
63
64
|
"mcp",
|
|
@@ -77,17 +78,17 @@
|
|
|
77
78
|
"no-api-key"
|
|
78
79
|
],
|
|
79
80
|
"dependencies": {
|
|
80
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
81
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
81
82
|
"cheerio": "^1.2.0",
|
|
82
|
-
"zod": "^4.
|
|
83
|
+
"zod": "^4.6.7"
|
|
83
84
|
},
|
|
84
85
|
"devDependencies": {
|
|
85
|
-
"@biomejs/biome": "2.
|
|
86
|
-
"@types/node": "
|
|
87
|
-
"@vitest/coverage-v8": "
|
|
88
|
-
"prettier": "3.9.
|
|
89
|
-
"tsup": "
|
|
90
|
-
"typescript": "
|
|
91
|
-
"vitest": "
|
|
86
|
+
"@biomejs/biome": "2.6.3",
|
|
87
|
+
"@types/node": "24.19.2",
|
|
88
|
+
"@vitest/coverage-v8": "5.0.5",
|
|
89
|
+
"prettier": "3.9.10",
|
|
90
|
+
"tsup": "9.0.0",
|
|
91
|
+
"typescript": "5.9.3",
|
|
92
|
+
"vitest": "5.0.5"
|
|
92
93
|
}
|
|
93
94
|
}
|
package/server.json
CHANGED
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
"url": "https://github.com/smeet666/mcp-lyricscom",
|
|
20
20
|
"source": "github"
|
|
21
21
|
},
|
|
22
|
-
"version": "2.0
|
|
22
|
+
"version": "2.1.0",
|
|
23
23
|
"packages": [
|
|
24
24
|
{
|
|
25
25
|
"registryType": "npm",
|
|
26
26
|
"identifier": "mcp-lyricscom",
|
|
27
|
-
"version": "2.0
|
|
27
|
+
"version": "2.1.0",
|
|
28
28
|
"transport": {
|
|
29
29
|
"type": "stdio"
|
|
30
30
|
},
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"registryType": "mcpb",
|
|
51
|
-
"identifier": "https://github.com/smeet666/mcp-lyricscom/releases/download/v2.0
|
|
52
|
-
"version": "2.0
|
|
51
|
+
"identifier": "https://github.com/smeet666/mcp-lyricscom/releases/download/v2.1.0/mcp-lyricscom-2.1.0.mcpb",
|
|
52
|
+
"version": "2.1.0",
|
|
53
53
|
"transport": {
|
|
54
54
|
"type": "stdio"
|
|
55
55
|
}
|