mcp-lyricscom 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +39 -10
- package/dist/index.js.map +1 -1
- package/dist/lyricscom/client.js +39 -10
- package/dist/lyricscom/client.js.map +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
|
-
var PKG_VERSION = "1.0.
|
|
7
|
+
var PKG_VERSION = "1.0.2";
|
|
8
8
|
var REPO_URL = "https://github.com/smeet666/mcp-lyricscom";
|
|
9
9
|
|
|
10
10
|
// src/config.ts
|
|
@@ -238,17 +238,24 @@ var RateLimiter = class {
|
|
|
238
238
|
}
|
|
239
239
|
/** Queue a task. Tasks run in call order, one at a time. */
|
|
240
240
|
schedule(task) {
|
|
241
|
-
const run = this.tail.then(async () =>
|
|
242
|
-
await this.waitForSlot();
|
|
243
|
-
this.lastStart = Date.now();
|
|
244
|
-
return task();
|
|
245
|
-
});
|
|
241
|
+
const run = this.tail.then(async () => task());
|
|
246
242
|
this.tail = run.then(
|
|
247
243
|
() => void 0,
|
|
248
244
|
() => void 0
|
|
249
245
|
);
|
|
250
246
|
return run;
|
|
251
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Wait for this request's slot, then claim it.
|
|
250
|
+
*
|
|
251
|
+
* Called once per upstream request rather than once per task, because a task
|
|
252
|
+
* runs a whole retry chain: stamping only its start would let the next task
|
|
253
|
+
* follow the chain's last request with no gap at all.
|
|
254
|
+
*/
|
|
255
|
+
async beforeRequest() {
|
|
256
|
+
await this.waitForSlot();
|
|
257
|
+
this.lastStart = Date.now();
|
|
258
|
+
}
|
|
252
259
|
/** Called after the site signals it is under load. */
|
|
253
260
|
penalize() {
|
|
254
261
|
const next = this.intervalMs === 0 ? 250 : this.intervalMs * 2;
|
|
@@ -261,7 +268,7 @@ var RateLimiter = class {
|
|
|
261
268
|
async waitForSlot() {
|
|
262
269
|
if (this.intervalMs === 0 || this.lastStart === 0) return;
|
|
263
270
|
const elapsed = Date.now() - this.lastStart;
|
|
264
|
-
const remaining = this.intervalMs - elapsed;
|
|
271
|
+
const remaining = Math.min(this.intervalMs, this.intervalMs - elapsed);
|
|
265
272
|
if (remaining > 0) await sleep(remaining);
|
|
266
273
|
}
|
|
267
274
|
};
|
|
@@ -283,15 +290,19 @@ async function fetchHtml(url, deps) {
|
|
|
283
290
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
284
291
|
return limiter.schedule(async () => {
|
|
285
292
|
let lastError;
|
|
293
|
+
let askedWaitMs = null;
|
|
286
294
|
for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {
|
|
287
295
|
if (attempt > 0) {
|
|
288
|
-
const delay = backoffDelay(attempt - 1);
|
|
296
|
+
const delay = Math.min(askedWaitMs ?? backoffDelay(attempt - 1), BACKOFF_MAX_MS);
|
|
297
|
+
askedWaitMs = null;
|
|
289
298
|
logger.info(`retry ${attempt}/${config.maxRetries} in ${delay}ms for ${url}`);
|
|
290
299
|
await sleep(delay);
|
|
291
300
|
}
|
|
292
301
|
let status;
|
|
293
302
|
let body;
|
|
303
|
+
let retryAfterMs = null;
|
|
294
304
|
try {
|
|
305
|
+
await limiter.beforeRequest();
|
|
295
306
|
const response = await doFetch(url, {
|
|
296
307
|
headers: {
|
|
297
308
|
"User-Agent": config.userAgent,
|
|
@@ -302,6 +313,7 @@ async function fetchHtml(url, deps) {
|
|
|
302
313
|
signal: AbortSignal.timeout(config.timeoutMs)
|
|
303
314
|
});
|
|
304
315
|
status = response.status;
|
|
316
|
+
retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
|
|
305
317
|
body = await response.text();
|
|
306
318
|
} catch (error) {
|
|
307
319
|
lastError = asTransportError(error, url);
|
|
@@ -318,7 +330,8 @@ async function fetchHtml(url, deps) {
|
|
|
318
330
|
logger.info(
|
|
319
331
|
`throttled (${verdict.reason}) on ${url}, interval now ${limiter.currentIntervalMs}ms`
|
|
320
332
|
);
|
|
321
|
-
|
|
333
|
+
askedWaitMs = retryAfterMs;
|
|
334
|
+
lastError = throttled(url, retryAfterMs ?? backoffDelay(attempt));
|
|
322
335
|
continue;
|
|
323
336
|
case "server-error":
|
|
324
337
|
lastError = new LyricsComError(
|
|
@@ -340,6 +353,14 @@ async function fetchHtml(url, deps) {
|
|
|
340
353
|
throw lastError ?? new LyricsComError("network_error", `Could not fetch ${url}.`, { url });
|
|
341
354
|
});
|
|
342
355
|
}
|
|
356
|
+
function parseRetryAfter(raw) {
|
|
357
|
+
if (!raw) return null;
|
|
358
|
+
const seconds = Number(raw.trim());
|
|
359
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1e3);
|
|
360
|
+
const when = Date.parse(raw);
|
|
361
|
+
if (Number.isNaN(when)) return null;
|
|
362
|
+
return Math.max(0, when - Date.now());
|
|
363
|
+
}
|
|
343
364
|
function asTransportError(error, url) {
|
|
344
365
|
if (error instanceof LyricsComError) return error;
|
|
345
366
|
const name = error instanceof Error ? error.name : "";
|
|
@@ -600,6 +621,14 @@ function extractBodyText($, body) {
|
|
|
600
621
|
}
|
|
601
622
|
|
|
602
623
|
// src/lyricscom/client.ts
|
|
624
|
+
function withGuarantees(config) {
|
|
625
|
+
const userAgent = /mozilla\/|applewebkit|chrome\/|safari\/|gecko/i.test(config.userAgent) ? `${config.userAgent} ${DEFAULT_USER_AGENT}` : config.userAgent;
|
|
626
|
+
return {
|
|
627
|
+
...config,
|
|
628
|
+
userAgent,
|
|
629
|
+
minIntervalMs: Math.max(MIN_ALLOWED_INTERVAL_MS, config.minIntervalMs)
|
|
630
|
+
};
|
|
631
|
+
}
|
|
603
632
|
var LyricsComClient = class {
|
|
604
633
|
config;
|
|
605
634
|
logger;
|
|
@@ -607,7 +636,7 @@ var LyricsComClient = class {
|
|
|
607
636
|
cache;
|
|
608
637
|
fetchImpl;
|
|
609
638
|
constructor(options = {}) {
|
|
610
|
-
this.config = options.config ?? loadConfig();
|
|
639
|
+
this.config = withGuarantees(options.config ?? loadConfig());
|
|
611
640
|
this.logger = options.logger ?? createLogger(this.config.logLevel);
|
|
612
641
|
this.limiter = new RateLimiter({ minIntervalMs: this.config.minIntervalMs });
|
|
613
642
|
this.cache = new TtlLruCache(this.config.cacheMaxEntries, this.config.cacheTtlMs);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/version.ts","../src/config.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/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 { 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 void server.close().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 * 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 = \"1.0.1\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-lyricscom\";\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 { 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() === \"\") return range.fallback;\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() === \"\") return DEFAULTS.minIntervalMs;\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 * 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 type { Config, Logger } from \"./config.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { LyricsComClient } from \"./lyricscom/client.js\";\nimport {\n getLyricsDescription,\n getLyricsInputShape,\n getLyricsOutputShape,\n runGetLyrics,\n} from \"./tools/getLyrics.js\";\nimport type { GetLyricsArgs } from \"./tools/getLyrics.js\";\nimport {\n runSearchLyrics,\n searchLyricsDescription,\n searchLyricsInputShape,\n searchLyricsOutputShape,\n} from \"./tools/searchLyrics.js\";\nimport type { SearchLyricsArgs } from \"./tools/searchLyrics.js\";\nimport {\n runSearchSongs,\n searchSongsDescription,\n searchSongsInputShape,\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: searchLyricsInputShape,\n outputSchema: 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: searchSongsInputShape,\n outputSchema: 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: getLyricsInputShape,\n outputSchema: 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) return undefined;\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) return;\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) break;\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\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\" | \"status-429\" | \"status-503\" | \"empty-body\" | \"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) return { kind: \"throttled\", reason: \"status-202\" };\n if (status === 429) return { kind: \"throttled\", reason: \"status-429\" };\n if (status === 503) return { kind: \"throttled\", reason: \"status-503\" };\n if (status === 403) return { kind: \"blocked\", status };\n if (status === 404) return { kind: \"not-found\" };\n if (status >= 500) return { kind: \"server-error\", status };\n\n const trimmed = body.trim();\n if (trimmed.length === 0) return { kind: \"throttled\", reason: \"empty-body\" };\n if (trimmed.length < MIN_PLAUSIBLE_HTML && !/<\\/html>/i.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 () => {\n await this.waitForSlot();\n this.lastStart = Date.now();\n return task();\n });\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 /** 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) return;\n const elapsed = Date.now() - this.lastStart;\n const remaining = this.intervalMs - elapsed;\n if (remaining > 0) await sleep(remaining);\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 { 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 limiter.schedule(async () => {\n let lastError: LyricsComError | undefined;\n\n for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {\n if (attempt > 0) {\n const delay = backoffDelay(attempt - 1);\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 try {\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 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 lastError = throttled(url, 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 }\n\n throw lastError ?? new LyricsComError(\"network_error\", `Could not fetch ${url}.`, { url });\n });\n}\n\nfunction asTransportError(error: unknown, url: string): LyricsComError {\n if (error instanceof LyricsComError) return error;\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 * as cheerio 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\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 $ = cheerio.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 = /\\b(1[89]\\d{2}|20\\d{2})\\b/.exec(raw);\n if (!match?.[1]) return null;\n return Number.parseInt(match[1], 10);\n}\n","/**\n * Text cleaning for scraped content: HTML entities, mojibake, whitespace.\n */\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) return text;\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) return match;\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 || !/[ÃÂ]/.test(text)) return text;\n try {\n const repaired = Buffer.from(text, \"latin1\").toString(\"utf8\");\n if (repaired.includes(\"�\")) return text;\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) return \"\";\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) return \"\";\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[out.length - 1] === \"\")) continue;\n out.push(line);\n }\n while (out.length > 0 && out[out.length - 1] === \"\") out.pop();\n return out.join(\"\\n\");\n}\n","/**\n * URL construction and validation for lyrics.com.\n */\n\nimport { invalidInput } from \"../errors.js\";\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) return `${BASE_URL}/lyrics/${encoded}`;\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:\") return false;\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 (/^https?:\\/\\//i.test(hrefOrUrl)) {\n if (!isLyricsComHost(hrefOrUrl)) return null;\n path = new URL(hrefOrUrl).pathname;\n }\n if (!path.startsWith(\"/\")) path = `/${path}`;\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 (!/^\\d+$/.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 * as cheerio 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\nexport interface ParseSongOptions {\n id: string;\n url: string;\n}\n\nexport function parseSongPage(html: string, options: ParseSongOptions): SongPage {\n const $ = cheerio.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/i.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($: cheerio.CheerioAPI, body: ReturnType<cheerio.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 cheerio.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 { createLogger, loadConfig } 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\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\nexport class LyricsComClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly limiter: RateLimiter;\n private readonly cache: TtlLruCache<string>;\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: LyricsComClientOptions = {}) {\n this.config = 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<string>(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 const { html, cached } = await this.fetchPage(url);\n return { data: parseSearchResults(html, { page, url }), cached };\n }\n\n async getSong(ref: { id?: string; url?: string }): Promise<FetchOutcome<SongPage>> {\n const { id, url } = resolveSongRef(ref);\n const { html, cached } = await this.fetchPage(url);\n return { data: parseSongPage(html, { id, url }), cached };\n }\n\n private async fetchPage(url: string): Promise<{ html: string; cached: boolean }> {\n const hit = this.cache.get(url);\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${url}`);\n return { html: hit, 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 // Only successful pages are cached; a throttled response never reaches here.\n this.cache.set(url, html);\n return { html, 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 { ok, toToolError, truncate } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\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 getLyricsInputShape = {\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(20000)\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};\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) return { slice: \"\", nextOffset: null };\n\n const remaining = text.slice(offset);\n if (remaining.length <= maxChars) return { slice: remaining, nextOffset: null };\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) ref.id = args.id;\n else if (args.url) ref.url = args.url;\n\n const { data } = await client.getSong(ref);\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 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 };\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 nextOffset !== null\n ? `Truncated: call again with offset=${nextOffset} to continue.`\n : \"Complete.\",\n structured.highlight\n ? structured.highlight.found\n ? `\"${structured.highlight.word}\" appears on line ${structured.highlight.line_number}.`\n : `\"${structured.highlight.word}\" does not appear in these lyrics.`\n : \"\",\n ]\n .filter(Boolean)\n .join(\"\\n\");\n\n return ok(structured, truncate(`${summary}\\n\\n${slice}`, 4000));\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) return -1;\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) return null;\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() === \"\") continue;\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) return null;\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) return null;\n if (line.length <= maxChars) return line;\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 * 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\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\nexport function ok(structured: Record<string, unknown>, text: string): ToolResult {\n return {\n content: [{ type: \"text\", text: truncate(text, MAX_TEXT_MIRROR_CHARS) }],\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) lines.push(`Hint: ${known.details.hint}`);\n if (known.details.url) lines.push(`URL: ${known.details.url}`);\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) return text;\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 { ok, renderResultList, songResultSchema, toSongResultOut, toToolError } 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\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 searchLyricsInputShape = {\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) notes.push(\"Served from this server's short-lived in-memory cache.\");\n\n const unique = dedupeSongs(data.results);\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) break;\n\n let text = song.snippet ?? \"\";\n\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) text = page.data.lyrics;\n } catch (error) {\n // A single unreadable song page must not sink the whole search.\n notes.push(`Could not verify \"${song.title}\" against its full lyrics.`);\n void error;\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\n ? matchedLineExcerpt(text, args.query, { maxChars: 160 })\n : null;\n kept.push({ song, excerpt });\n }\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 + 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 const footer = data.hasMore\n ? `\\n\\nMore results available: call again with page=${args.page + 1}.`\n : \"\";\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)) continue;\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 { ok, renderResultList, songResultSchema, toSongResultOut, toToolError } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\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 searchSongsInputShape = {\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) return 0;\n if (a === b) return 3;\n if (a.startsWith(b)) return 2;\n if (a.includes(b)) return 1;\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) notes.push(\"Served from this server's short-lived in-memory cache.\");\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 + 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} matching \"${args.title}\":`\n : `No song on page ${args.page} matched \"${args.title}\".`;\n const footer = data.hasMore\n ? `\\n\\nMore results available: call again with page=${args.page + 1}.`\n : \"\";\n\n return ok(structured, `${header}\\n${renderResultList(results)}${footer}`);\n } catch (error) {\n return toToolError(error);\n }\n}\n"],"mappings":"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","names":["cheerio","z","z","structured","z","z","z","z"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/version.ts","../src/config.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/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 { 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 void server.close().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 * 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 = \"1.0.2\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-lyricscom\";\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 { 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() === \"\") return range.fallback;\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() === \"\") return DEFAULTS.minIntervalMs;\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 * 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 type { Config, Logger } from \"./config.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { LyricsComClient } from \"./lyricscom/client.js\";\nimport {\n getLyricsDescription,\n getLyricsInputShape,\n getLyricsOutputShape,\n runGetLyrics,\n} from \"./tools/getLyrics.js\";\nimport type { GetLyricsArgs } from \"./tools/getLyrics.js\";\nimport {\n runSearchLyrics,\n searchLyricsDescription,\n searchLyricsInputShape,\n searchLyricsOutputShape,\n} from \"./tools/searchLyrics.js\";\nimport type { SearchLyricsArgs } from \"./tools/searchLyrics.js\";\nimport {\n runSearchSongs,\n searchSongsDescription,\n searchSongsInputShape,\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: searchLyricsInputShape,\n outputSchema: 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: searchSongsInputShape,\n outputSchema: 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: getLyricsInputShape,\n outputSchema: 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) return undefined;\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) return;\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) break;\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\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\" | \"status-429\" | \"status-503\" | \"empty-body\" | \"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) return { kind: \"throttled\", reason: \"status-202\" };\n if (status === 429) return { kind: \"throttled\", reason: \"status-429\" };\n if (status === 503) return { kind: \"throttled\", reason: \"status-503\" };\n if (status === 403) return { kind: \"blocked\", status };\n if (status === 404) return { kind: \"not-found\" };\n if (status >= 500) return { kind: \"server-error\", status };\n\n const trimmed = body.trim();\n if (trimmed.length === 0) return { kind: \"throttled\", reason: \"empty-body\" };\n if (trimmed.length < MIN_PLAUSIBLE_HTML && !/<\\/html>/i.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) return;\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) await sleep(remaining);\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 { 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 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 }\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) return null;\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1000);\n const when = Date.parse(raw);\n if (Number.isNaN(when)) return null;\n return Math.max(0, when - Date.now());\n}\n\nfunction asTransportError(error: unknown, url: string): LyricsComError {\n if (error instanceof LyricsComError) return error;\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 * as cheerio 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\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 $ = cheerio.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 = /\\b(1[89]\\d{2}|20\\d{2})\\b/.exec(raw);\n if (!match?.[1]) return null;\n return Number.parseInt(match[1], 10);\n}\n","/**\n * Text cleaning for scraped content: HTML entities, mojibake, whitespace.\n */\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) return text;\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) return match;\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 || !/[ÃÂ]/.test(text)) return text;\n try {\n const repaired = Buffer.from(text, \"latin1\").toString(\"utf8\");\n if (repaired.includes(\"�\")) return text;\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) return \"\";\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) return \"\";\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[out.length - 1] === \"\")) continue;\n out.push(line);\n }\n while (out.length > 0 && out[out.length - 1] === \"\") out.pop();\n return out.join(\"\\n\");\n}\n","/**\n * URL construction and validation for lyrics.com.\n */\n\nimport { invalidInput } from \"../errors.js\";\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) return `${BASE_URL}/lyrics/${encoded}`;\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:\") return false;\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 (/^https?:\\/\\//i.test(hrefOrUrl)) {\n if (!isLyricsComHost(hrefOrUrl)) return null;\n path = new URL(hrefOrUrl).pathname;\n }\n if (!path.startsWith(\"/\")) path = `/${path}`;\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 (!/^\\d+$/.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 * as cheerio 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\nexport interface ParseSongOptions {\n id: string;\n url: string;\n}\n\nexport function parseSongPage(html: string, options: ParseSongOptions): SongPage {\n const $ = cheerio.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/i.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($: cheerio.CheerioAPI, body: ReturnType<cheerio.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 cheerio.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\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 = /mozilla\\/|applewebkit|chrome\\/|safari\\/|gecko/i.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<string>;\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<string>(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 const { html, cached } = await this.fetchPage(url);\n return { data: parseSearchResults(html, { page, url }), cached };\n }\n\n async getSong(ref: { id?: string; url?: string }): Promise<FetchOutcome<SongPage>> {\n const { id, url } = resolveSongRef(ref);\n const { html, cached } = await this.fetchPage(url);\n return { data: parseSongPage(html, { id, url }), cached };\n }\n\n private async fetchPage(url: string): Promise<{ html: string; cached: boolean }> {\n const hit = this.cache.get(url);\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${url}`);\n return { html: hit, 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 // Only successful pages are cached; a throttled response never reaches here.\n this.cache.set(url, html);\n return { html, 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 { ok, toToolError, truncate } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\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 getLyricsInputShape = {\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(20000)\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};\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) return { slice: \"\", nextOffset: null };\n\n const remaining = text.slice(offset);\n if (remaining.length <= maxChars) return { slice: remaining, nextOffset: null };\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) ref.id = args.id;\n else if (args.url) ref.url = args.url;\n\n const { data } = await client.getSong(ref);\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 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 };\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 nextOffset !== null\n ? `Truncated: call again with offset=${nextOffset} to continue.`\n : \"Complete.\",\n structured.highlight\n ? structured.highlight.found\n ? `\"${structured.highlight.word}\" appears on line ${structured.highlight.line_number}.`\n : `\"${structured.highlight.word}\" does not appear in these lyrics.`\n : \"\",\n ]\n .filter(Boolean)\n .join(\"\\n\");\n\n return ok(structured, truncate(`${summary}\\n\\n${slice}`, 4000));\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) return -1;\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) return null;\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() === \"\") continue;\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) return null;\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) return null;\n if (line.length <= maxChars) return line;\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 * 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\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\nexport function ok(structured: Record<string, unknown>, text: string): ToolResult {\n return {\n content: [{ type: \"text\", text: truncate(text, MAX_TEXT_MIRROR_CHARS) }],\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) lines.push(`Hint: ${known.details.hint}`);\n if (known.details.url) lines.push(`URL: ${known.details.url}`);\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) return text;\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 { ok, renderResultList, songResultSchema, toSongResultOut, toToolError } 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\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 searchLyricsInputShape = {\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) notes.push(\"Served from this server's short-lived in-memory cache.\");\n\n const unique = dedupeSongs(data.results);\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) break;\n\n let text = song.snippet ?? \"\";\n\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) text = page.data.lyrics;\n } catch (error) {\n // A single unreadable song page must not sink the whole search.\n notes.push(`Could not verify \"${song.title}\" against its full lyrics.`);\n void error;\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\n ? matchedLineExcerpt(text, args.query, { maxChars: 160 })\n : null;\n kept.push({ song, excerpt });\n }\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 + 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 const footer = data.hasMore\n ? `\\n\\nMore results available: call again with page=${args.page + 1}.`\n : \"\";\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)) continue;\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 { ok, renderResultList, songResultSchema, toSongResultOut, toToolError } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\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 searchSongsInputShape = {\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) return 0;\n if (a === b) return 3;\n if (a.startsWith(b)) return 2;\n if (a.includes(b)) return 1;\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) notes.push(\"Served from this server's short-lived in-memory cache.\");\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 + 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} matching \"${args.title}\":`\n : `No song on page ${args.page} matched \"${args.title}\".`;\n const footer = data.hasMore\n ? `\\n\\nMore results available: call again with page=${args.page + 1}.`\n : \"\";\n\n return ok(structured, `${header}\\n${renderResultList(results)}${footer}`);\n } catch (error) {\n return toToolError(error);\n }\n}\n"],"mappings":"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","names":["cheerio","z","z","structured","z","z","z","z"]}
|
package/dist/lyricscom/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/version.ts
|
|
2
|
-
var PKG_VERSION = "1.0.
|
|
2
|
+
var PKG_VERSION = "1.0.2";
|
|
3
3
|
var REPO_URL = "https://github.com/smeet666/mcp-lyricscom";
|
|
4
4
|
|
|
5
5
|
// src/config.ts
|
|
@@ -230,17 +230,24 @@ var RateLimiter = class {
|
|
|
230
230
|
}
|
|
231
231
|
/** Queue a task. Tasks run in call order, one at a time. */
|
|
232
232
|
schedule(task) {
|
|
233
|
-
const run = this.tail.then(async () =>
|
|
234
|
-
await this.waitForSlot();
|
|
235
|
-
this.lastStart = Date.now();
|
|
236
|
-
return task();
|
|
237
|
-
});
|
|
233
|
+
const run = this.tail.then(async () => task());
|
|
238
234
|
this.tail = run.then(
|
|
239
235
|
() => void 0,
|
|
240
236
|
() => void 0
|
|
241
237
|
);
|
|
242
238
|
return run;
|
|
243
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Wait for this request's slot, then claim it.
|
|
242
|
+
*
|
|
243
|
+
* Called once per upstream request rather than once per task, because a task
|
|
244
|
+
* runs a whole retry chain: stamping only its start would let the next task
|
|
245
|
+
* follow the chain's last request with no gap at all.
|
|
246
|
+
*/
|
|
247
|
+
async beforeRequest() {
|
|
248
|
+
await this.waitForSlot();
|
|
249
|
+
this.lastStart = Date.now();
|
|
250
|
+
}
|
|
244
251
|
/** Called after the site signals it is under load. */
|
|
245
252
|
penalize() {
|
|
246
253
|
const next = this.intervalMs === 0 ? 250 : this.intervalMs * 2;
|
|
@@ -253,7 +260,7 @@ var RateLimiter = class {
|
|
|
253
260
|
async waitForSlot() {
|
|
254
261
|
if (this.intervalMs === 0 || this.lastStart === 0) return;
|
|
255
262
|
const elapsed = Date.now() - this.lastStart;
|
|
256
|
-
const remaining = this.intervalMs - elapsed;
|
|
263
|
+
const remaining = Math.min(this.intervalMs, this.intervalMs - elapsed);
|
|
257
264
|
if (remaining > 0) await sleep(remaining);
|
|
258
265
|
}
|
|
259
266
|
};
|
|
@@ -275,15 +282,19 @@ async function fetchHtml(url, deps) {
|
|
|
275
282
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
276
283
|
return limiter.schedule(async () => {
|
|
277
284
|
let lastError;
|
|
285
|
+
let askedWaitMs = null;
|
|
278
286
|
for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {
|
|
279
287
|
if (attempt > 0) {
|
|
280
|
-
const delay = backoffDelay(attempt - 1);
|
|
288
|
+
const delay = Math.min(askedWaitMs ?? backoffDelay(attempt - 1), BACKOFF_MAX_MS);
|
|
289
|
+
askedWaitMs = null;
|
|
281
290
|
logger.info(`retry ${attempt}/${config.maxRetries} in ${delay}ms for ${url}`);
|
|
282
291
|
await sleep(delay);
|
|
283
292
|
}
|
|
284
293
|
let status;
|
|
285
294
|
let body;
|
|
295
|
+
let retryAfterMs = null;
|
|
286
296
|
try {
|
|
297
|
+
await limiter.beforeRequest();
|
|
287
298
|
const response = await doFetch(url, {
|
|
288
299
|
headers: {
|
|
289
300
|
"User-Agent": config.userAgent,
|
|
@@ -294,6 +305,7 @@ async function fetchHtml(url, deps) {
|
|
|
294
305
|
signal: AbortSignal.timeout(config.timeoutMs)
|
|
295
306
|
});
|
|
296
307
|
status = response.status;
|
|
308
|
+
retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
|
|
297
309
|
body = await response.text();
|
|
298
310
|
} catch (error) {
|
|
299
311
|
lastError = asTransportError(error, url);
|
|
@@ -310,7 +322,8 @@ async function fetchHtml(url, deps) {
|
|
|
310
322
|
logger.info(
|
|
311
323
|
`throttled (${verdict.reason}) on ${url}, interval now ${limiter.currentIntervalMs}ms`
|
|
312
324
|
);
|
|
313
|
-
|
|
325
|
+
askedWaitMs = retryAfterMs;
|
|
326
|
+
lastError = throttled(url, retryAfterMs ?? backoffDelay(attempt));
|
|
314
327
|
continue;
|
|
315
328
|
case "server-error":
|
|
316
329
|
lastError = new LyricsComError(
|
|
@@ -332,6 +345,14 @@ async function fetchHtml(url, deps) {
|
|
|
332
345
|
throw lastError ?? new LyricsComError("network_error", `Could not fetch ${url}.`, { url });
|
|
333
346
|
});
|
|
334
347
|
}
|
|
348
|
+
function parseRetryAfter(raw) {
|
|
349
|
+
if (!raw) return null;
|
|
350
|
+
const seconds = Number(raw.trim());
|
|
351
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1e3);
|
|
352
|
+
const when = Date.parse(raw);
|
|
353
|
+
if (Number.isNaN(when)) return null;
|
|
354
|
+
return Math.max(0, when - Date.now());
|
|
355
|
+
}
|
|
335
356
|
function asTransportError(error, url) {
|
|
336
357
|
if (error instanceof LyricsComError) return error;
|
|
337
358
|
const name = error instanceof Error ? error.name : "";
|
|
@@ -592,6 +613,14 @@ function extractBodyText($, body) {
|
|
|
592
613
|
}
|
|
593
614
|
|
|
594
615
|
// src/lyricscom/client.ts
|
|
616
|
+
function withGuarantees(config) {
|
|
617
|
+
const userAgent = /mozilla\/|applewebkit|chrome\/|safari\/|gecko/i.test(config.userAgent) ? `${config.userAgent} ${DEFAULT_USER_AGENT}` : config.userAgent;
|
|
618
|
+
return {
|
|
619
|
+
...config,
|
|
620
|
+
userAgent,
|
|
621
|
+
minIntervalMs: Math.max(MIN_ALLOWED_INTERVAL_MS, config.minIntervalMs)
|
|
622
|
+
};
|
|
623
|
+
}
|
|
595
624
|
var LyricsComClient = class {
|
|
596
625
|
config;
|
|
597
626
|
logger;
|
|
@@ -599,7 +628,7 @@ var LyricsComClient = class {
|
|
|
599
628
|
cache;
|
|
600
629
|
fetchImpl;
|
|
601
630
|
constructor(options = {}) {
|
|
602
|
-
this.config = options.config ?? loadConfig();
|
|
631
|
+
this.config = withGuarantees(options.config ?? loadConfig());
|
|
603
632
|
this.logger = options.logger ?? createLogger(this.config.logLevel);
|
|
604
633
|
this.limiter = new RateLimiter({ minIntervalMs: this.config.minIntervalMs });
|
|
605
634
|
this.cache = new TtlLruCache(this.config.cacheMaxEntries, this.config.cacheTtlMs);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/version.ts","../../src/config.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 * 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 = \"1.0.1\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-lyricscom\";\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 { 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() === \"\") return range.fallback;\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() === \"\") return DEFAULTS.minIntervalMs;\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 * 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) return undefined;\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) return;\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) break;\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\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\" | \"status-429\" | \"status-503\" | \"empty-body\" | \"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) return { kind: \"throttled\", reason: \"status-202\" };\n if (status === 429) return { kind: \"throttled\", reason: \"status-429\" };\n if (status === 503) return { kind: \"throttled\", reason: \"status-503\" };\n if (status === 403) return { kind: \"blocked\", status };\n if (status === 404) return { kind: \"not-found\" };\n if (status >= 500) return { kind: \"server-error\", status };\n\n const trimmed = body.trim();\n if (trimmed.length === 0) return { kind: \"throttled\", reason: \"empty-body\" };\n if (trimmed.length < MIN_PLAUSIBLE_HTML && !/<\\/html>/i.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 () => {\n await this.waitForSlot();\n this.lastStart = Date.now();\n return task();\n });\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 /** 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) return;\n const elapsed = Date.now() - this.lastStart;\n const remaining = this.intervalMs - elapsed;\n if (remaining > 0) await sleep(remaining);\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 { 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 limiter.schedule(async () => {\n let lastError: LyricsComError | undefined;\n\n for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {\n if (attempt > 0) {\n const delay = backoffDelay(attempt - 1);\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 try {\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 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 lastError = throttled(url, 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 }\n\n throw lastError ?? new LyricsComError(\"network_error\", `Could not fetch ${url}.`, { url });\n });\n}\n\nfunction asTransportError(error: unknown, url: string): LyricsComError {\n if (error instanceof LyricsComError) return error;\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 * as cheerio 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\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 $ = cheerio.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 = /\\b(1[89]\\d{2}|20\\d{2})\\b/.exec(raw);\n if (!match?.[1]) return null;\n return Number.parseInt(match[1], 10);\n}\n","/**\n * Text cleaning for scraped content: HTML entities, mojibake, whitespace.\n */\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) return text;\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) return match;\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 || !/[ÃÂ]/.test(text)) return text;\n try {\n const repaired = Buffer.from(text, \"latin1\").toString(\"utf8\");\n if (repaired.includes(\"�\")) return text;\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) return \"\";\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) return \"\";\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[out.length - 1] === \"\")) continue;\n out.push(line);\n }\n while (out.length > 0 && out[out.length - 1] === \"\") out.pop();\n return out.join(\"\\n\");\n}\n","/**\n * URL construction and validation for lyrics.com.\n */\n\nimport { invalidInput } from \"../errors.js\";\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) return `${BASE_URL}/lyrics/${encoded}`;\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:\") return false;\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 (/^https?:\\/\\//i.test(hrefOrUrl)) {\n if (!isLyricsComHost(hrefOrUrl)) return null;\n path = new URL(hrefOrUrl).pathname;\n }\n if (!path.startsWith(\"/\")) path = `/${path}`;\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 (!/^\\d+$/.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 * as cheerio 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\nexport interface ParseSongOptions {\n id: string;\n url: string;\n}\n\nexport function parseSongPage(html: string, options: ParseSongOptions): SongPage {\n const $ = cheerio.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/i.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($: cheerio.CheerioAPI, body: ReturnType<cheerio.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 cheerio.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 { createLogger, loadConfig } 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\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\nexport class LyricsComClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly limiter: RateLimiter;\n private readonly cache: TtlLruCache<string>;\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: LyricsComClientOptions = {}) {\n this.config = 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<string>(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 const { html, cached } = await this.fetchPage(url);\n return { data: parseSearchResults(html, { page, url }), cached };\n }\n\n async getSong(ref: { id?: string; url?: string }): Promise<FetchOutcome<SongPage>> {\n const { id, url } = resolveSongRef(ref);\n const { html, cached } = await this.fetchPage(url);\n return { data: parseSongPage(html, { id, url }), cached };\n }\n\n private async fetchPage(url: string): Promise<{ html: string; cached: boolean }> {\n const hit = this.cache.get(url);\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${url}`);\n return { html: hit, 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 // Only successful pages are cached; a throttled response never reaches here.\n this.cache.set(url, html);\n return { html, cached: false };\n }\n}\n"],"mappings":"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","names":["cheerio"]}
|
|
1
|
+
{"version":3,"sources":["../../src/version.ts","../../src/config.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 * 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 = \"1.0.2\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-lyricscom\";\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 { 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() === \"\") return range.fallback;\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() === \"\") return DEFAULTS.minIntervalMs;\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 * 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) return undefined;\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) return;\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) break;\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\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\" | \"status-429\" | \"status-503\" | \"empty-body\" | \"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) return { kind: \"throttled\", reason: \"status-202\" };\n if (status === 429) return { kind: \"throttled\", reason: \"status-429\" };\n if (status === 503) return { kind: \"throttled\", reason: \"status-503\" };\n if (status === 403) return { kind: \"blocked\", status };\n if (status === 404) return { kind: \"not-found\" };\n if (status >= 500) return { kind: \"server-error\", status };\n\n const trimmed = body.trim();\n if (trimmed.length === 0) return { kind: \"throttled\", reason: \"empty-body\" };\n if (trimmed.length < MIN_PLAUSIBLE_HTML && !/<\\/html>/i.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) return;\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) await sleep(remaining);\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 { 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 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 }\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) return null;\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1000);\n const when = Date.parse(raw);\n if (Number.isNaN(when)) return null;\n return Math.max(0, when - Date.now());\n}\n\nfunction asTransportError(error: unknown, url: string): LyricsComError {\n if (error instanceof LyricsComError) return error;\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 * as cheerio 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\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 $ = cheerio.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 = /\\b(1[89]\\d{2}|20\\d{2})\\b/.exec(raw);\n if (!match?.[1]) return null;\n return Number.parseInt(match[1], 10);\n}\n","/**\n * Text cleaning for scraped content: HTML entities, mojibake, whitespace.\n */\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) return text;\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) return match;\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 || !/[ÃÂ]/.test(text)) return text;\n try {\n const repaired = Buffer.from(text, \"latin1\").toString(\"utf8\");\n if (repaired.includes(\"�\")) return text;\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) return \"\";\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) return \"\";\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[out.length - 1] === \"\")) continue;\n out.push(line);\n }\n while (out.length > 0 && out[out.length - 1] === \"\") out.pop();\n return out.join(\"\\n\");\n}\n","/**\n * URL construction and validation for lyrics.com.\n */\n\nimport { invalidInput } from \"../errors.js\";\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) return `${BASE_URL}/lyrics/${encoded}`;\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:\") return false;\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 (/^https?:\\/\\//i.test(hrefOrUrl)) {\n if (!isLyricsComHost(hrefOrUrl)) return null;\n path = new URL(hrefOrUrl).pathname;\n }\n if (!path.startsWith(\"/\")) path = `/${path}`;\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 (!/^\\d+$/.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 * as cheerio 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\nexport interface ParseSongOptions {\n id: string;\n url: string;\n}\n\nexport function parseSongPage(html: string, options: ParseSongOptions): SongPage {\n const $ = cheerio.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/i.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($: cheerio.CheerioAPI, body: ReturnType<cheerio.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 cheerio.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\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 = /mozilla\\/|applewebkit|chrome\\/|safari\\/|gecko/i.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<string>;\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<string>(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 const { html, cached } = await this.fetchPage(url);\n return { data: parseSearchResults(html, { page, url }), cached };\n }\n\n async getSong(ref: { id?: string; url?: string }): Promise<FetchOutcome<SongPage>> {\n const { id, url } = resolveSongRef(ref);\n const { html, cached } = await this.fetchPage(url);\n return { data: parseSongPage(html, { id, url }), cached };\n }\n\n private async fetchPage(url: string): Promise<{ html: string; cached: boolean }> {\n const hit = this.cache.get(url);\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${url}`);\n return { html: hit, 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 // Only successful pages are cached; a throttled response never reaches here.\n this.cache.set(url, html);\n return { html, cached: false };\n }\n}\n"],"mappings":"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","names":["cheerio"]}
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/smeet666/mcp-lyricscom",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "mcp-lyricscom",
|
|
14
|
-
"version": "1.0.
|
|
14
|
+
"version": "1.0.2",
|
|
15
15
|
"transport": { "type": "stdio" },
|
|
16
16
|
"environmentVariables": [
|
|
17
17
|
{
|