mcp-lyricscom 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.1";
7
+ var PKG_VERSION = "1.1.0";
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
- lastError = throttled(url, backoffDelay(attempt));
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);
@@ -615,19 +644,26 @@ var LyricsComClient = class {
615
644
  }
616
645
  async search(term, page) {
617
646
  const url = buildSearchUrl(term, page);
618
- const { html, cached } = await this.fetchPage(url);
619
- return { data: parseSearchResults(html, { page, url }), cached };
647
+ return this.fetchParsed(url, (html) => parseSearchResults(html, { page, url }));
620
648
  }
621
649
  async getSong(ref) {
622
650
  const { id, url } = resolveSongRef(ref);
623
- const { html, cached } = await this.fetchPage(url);
624
- return { data: parseSongPage(html, { id, url }), cached };
651
+ return this.fetchParsed(url, (html) => parseSongPage(html, { id, url }));
625
652
  }
626
- async fetchPage(url) {
653
+ /**
654
+ * Fetch, parse, then cache. In that order: a page that could not be read is
655
+ * never stored, so a bad minute at lyrics.com cannot be replayed from memory for
656
+ * the rest of the cache lifetime, leaving the tool unable to recover after
657
+ * the site comes back.
658
+ *
659
+ * The cached value is the parsed result rather than the raw page, which also
660
+ * keeps a few hundred kilobytes of markup per entry out of memory.
661
+ */
662
+ async fetchParsed(url, parse) {
627
663
  const hit = this.cache.get(url);
628
664
  if (hit !== void 0) {
629
665
  this.logger.debug(`cache hit ${url}`);
630
- return { html: hit, cached: true };
666
+ return { data: hit, cached: true };
631
667
  }
632
668
  const html = await fetchHtml(url, {
633
669
  config: this.config,
@@ -635,8 +671,9 @@ var LyricsComClient = class {
635
671
  logger: this.logger,
636
672
  ...this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}
637
673
  });
638
- this.cache.set(url, html);
639
- return { html, cached: false };
674
+ const data = parse(html);
675
+ this.cache.set(url, data);
676
+ return { data, cached: false };
640
677
  }
641
678
  };
642
679
 
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.1.0\";\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<unknown>;\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: LyricsComClientOptions = {}) {\n this.config = withGuarantees(options.config ?? loadConfig());\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.limiter = new RateLimiter({ minIntervalMs: this.config.minIntervalMs });\n this.cache = new TtlLruCache<unknown>(this.config.cacheMaxEntries, this.config.cacheTtlMs);\n this.fetchImpl = options.fetchImpl;\n }\n\n async search(term: string, page: number): Promise<FetchOutcome<SearchPage>> {\n const url = buildSearchUrl(term, page);\n return this.fetchParsed(url, (html) => parseSearchResults(html, { page, url }));\n }\n\n async getSong(ref: { id?: string; url?: string }): Promise<FetchOutcome<SongPage>> {\n const { id, url } = resolveSongRef(ref);\n return this.fetchParsed(url, (html) => parseSongPage(html, { id, url }));\n }\n\n /**\n * Fetch, parse, then cache. In that order: a page that could not be read is\n * never stored, so a bad minute at lyrics.com cannot be replayed from memory for\n * the rest of the cache lifetime, leaving the tool unable to recover after\n * the site comes back.\n *\n * The cached value is the parsed result rather than the raw page, which also\n * keeps a few hundred kilobytes of markup per entry out of memory.\n */\n private async fetchParsed<T>(url: string, parse: (html: string) => T): Promise<FetchOutcome<T>> {\n const hit = this.cache.get(url);\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${url}`);\n return { data: hit as T, cached: true };\n }\n\n const html = await fetchHtml(url, {\n config: this.config,\n limiter: this.limiter,\n logger: this.logger,\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n });\n\n const data = parse(html);\n this.cache.set(url, data);\n return { data, cached: false };\n }\n}\n","/**\n * get_lyrics: fetch the full lyrics of one song.\n */\n\nimport { z } from \"zod\";\nimport type { LyricsComClient } from \"../lyricscom/client.js\";\nimport { findMatchingLine } from \"../text/excerpt.js\";\nimport { 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"]}
@@ -85,7 +85,16 @@ declare class LyricsComClient {
85
85
  id?: string;
86
86
  url?: string;
87
87
  }): Promise<FetchOutcome<SongPage>>;
88
- private fetchPage;
88
+ /**
89
+ * Fetch, parse, then cache. In that order: a page that could not be read is
90
+ * never stored, so a bad minute at lyrics.com cannot be replayed from memory for
91
+ * the rest of the cache lifetime, leaving the tool unable to recover after
92
+ * the site comes back.
93
+ *
94
+ * The cached value is the parsed result rather than the raw page, which also
95
+ * keeps a few hundred kilobytes of markup per entry out of memory.
96
+ */
97
+ private fetchParsed;
89
98
  }
90
99
 
91
100
  export { type FetchOutcome, LyricsComClient, type LyricsComClientOptions };
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var PKG_VERSION = "1.0.1";
2
+ var PKG_VERSION = "1.1.0";
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
- lastError = throttled(url, backoffDelay(attempt));
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);
@@ -607,19 +636,26 @@ var LyricsComClient = class {
607
636
  }
608
637
  async search(term, page) {
609
638
  const url = buildSearchUrl(term, page);
610
- const { html, cached } = await this.fetchPage(url);
611
- return { data: parseSearchResults(html, { page, url }), cached };
639
+ return this.fetchParsed(url, (html) => parseSearchResults(html, { page, url }));
612
640
  }
613
641
  async getSong(ref) {
614
642
  const { id, url } = resolveSongRef(ref);
615
- const { html, cached } = await this.fetchPage(url);
616
- return { data: parseSongPage(html, { id, url }), cached };
643
+ return this.fetchParsed(url, (html) => parseSongPage(html, { id, url }));
617
644
  }
618
- async fetchPage(url) {
645
+ /**
646
+ * Fetch, parse, then cache. In that order: a page that could not be read is
647
+ * never stored, so a bad minute at lyrics.com cannot be replayed from memory for
648
+ * the rest of the cache lifetime, leaving the tool unable to recover after
649
+ * the site comes back.
650
+ *
651
+ * The cached value is the parsed result rather than the raw page, which also
652
+ * keeps a few hundred kilobytes of markup per entry out of memory.
653
+ */
654
+ async fetchParsed(url, parse) {
619
655
  const hit = this.cache.get(url);
620
656
  if (hit !== void 0) {
621
657
  this.logger.debug(`cache hit ${url}`);
622
- return { html: hit, cached: true };
658
+ return { data: hit, cached: true };
623
659
  }
624
660
  const html = await fetchHtml(url, {
625
661
  config: this.config,
@@ -627,8 +663,9 @@ var LyricsComClient = class {
627
663
  logger: this.logger,
628
664
  ...this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}
629
665
  });
630
- this.cache.set(url, html);
631
- return { html, cached: false };
666
+ const data = parse(html);
667
+ this.cache.set(url, data);
668
+ return { data, cached: false };
632
669
  }
633
670
  };
634
671
  export {
@@ -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.1.0\";\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<unknown>;\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: LyricsComClientOptions = {}) {\n this.config = withGuarantees(options.config ?? loadConfig());\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.limiter = new RateLimiter({ minIntervalMs: this.config.minIntervalMs });\n this.cache = new TtlLruCache<unknown>(this.config.cacheMaxEntries, this.config.cacheTtlMs);\n this.fetchImpl = options.fetchImpl;\n }\n\n async search(term: string, page: number): Promise<FetchOutcome<SearchPage>> {\n const url = buildSearchUrl(term, page);\n return this.fetchParsed(url, (html) => parseSearchResults(html, { page, url }));\n }\n\n async getSong(ref: { id?: string; url?: string }): Promise<FetchOutcome<SongPage>> {\n const { id, url } = resolveSongRef(ref);\n return this.fetchParsed(url, (html) => parseSongPage(html, { id, url }));\n }\n\n /**\n * Fetch, parse, then cache. In that order: a page that could not be read is\n * never stored, so a bad minute at lyrics.com cannot be replayed from memory for\n * the rest of the cache lifetime, leaving the tool unable to recover after\n * the site comes back.\n *\n * The cached value is the parsed result rather than the raw page, which also\n * keeps a few hundred kilobytes of markup per entry out of memory.\n */\n private async fetchParsed<T>(url: string, parse: (html: string) => T): Promise<FetchOutcome<T>> {\n const hit = this.cache.get(url);\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${url}`);\n return { data: hit as T, cached: true };\n }\n\n const html = await fetchHtml(url, {\n config: this.config,\n limiter: this.limiter,\n logger: this.logger,\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n });\n\n const data = parse(html);\n this.cache.set(url, data);\n return { data, cached: false };\n }\n}\n"],"mappings":"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","names":["cheerio"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-lyricscom",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for lyrics.com. Search song lyrics by word or by title, and fetch full lyrics. No API key required.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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.1",
9
+ "version": "1.1.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "mcp-lyricscom",
14
- "version": "1.0.1",
14
+ "version": "1.1.0",
15
15
  "transport": { "type": "stdio" },
16
16
  "environmentVariables": [
17
17
  {