mr-magic-mcp-server 0.5.0 → 0.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-magic-mcp-server",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Lyrics MCP server connecting LRCLIB, Genius, Musixmatch, and Melon",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -64,8 +64,10 @@
64
64
  "@dotenvx/dotenvx": "^1.65.0",
65
65
  "@modelcontextprotocol/sdk": "^1.29.0",
66
66
  "axios": "^1.16.0",
67
- "cheerio": "^1.2.0",
68
- "commander": "^14.0.3"
67
+ "commander": "^14.0.3",
68
+ "css-select": "^6.0.0",
69
+ "domutils": "^3.2.2",
70
+ "htmlparser2": "^10.1.0"
69
71
  },
70
72
  "devDependencies": {
71
73
  "eslint": "^10.3.0",
@@ -85,8 +87,7 @@
85
87
  "hasown": "npm:@socketregistry/hasown@^1",
86
88
  "object-assign": "npm:@socketregistry/object-assign@^1",
87
89
  "safer-buffer": "npm:@socketregistry/safer-buffer@^1",
88
- "side-channel": "npm:@socketregistry/side-channel@^1",
89
- "encoding-sniffer": "^1.0.2"
90
+ "side-channel": "npm:@socketregistry/side-channel@^1"
90
91
  },
91
92
  "resolutions": {
92
93
  "entities": "^7.0.1",
@@ -99,7 +100,6 @@
99
100
  "hasown": "npm:@socketregistry/hasown@^1",
100
101
  "object-assign": "npm:@socketregistry/object-assign@^1",
101
102
  "safer-buffer": "npm:@socketregistry/safer-buffer@^1",
102
- "side-channel": "npm:@socketregistry/side-channel@^1",
103
- "encoding-sniffer": "^1.0.2"
103
+ "side-channel": "npm:@socketregistry/side-channel@^1"
104
104
  }
105
105
  }
@@ -1,5 +1,7 @@
1
1
  import axios from 'axios';
2
- import * as cheerio from 'cheerio';
2
+ import { selectAll } from 'css-select';
3
+ import * as DomUtils from 'domutils';
4
+ import { parseDocument } from 'htmlparser2';
3
5
 
4
6
  import { normalizeLyricRecord } from '../provider-result-schema.js';
5
7
  import { assertEnv, getEnvValue } from '../utils/config.js';
@@ -119,7 +121,7 @@ export async function fetchFromGenius(track) {
119
121
  return primary;
120
122
  }
121
123
 
122
- function normalizeNodeText($node) {
124
+ function normalizeNodeText(node) {
123
125
  const lines = [];
124
126
  const traverse = (node) => {
125
127
  if (!node) return;
@@ -145,7 +147,7 @@ function normalizeNodeText($node) {
145
147
  }
146
148
  };
147
149
 
148
- traverse($node[0]);
150
+ traverse(node);
149
151
 
150
152
  return lines
151
153
  .join(' ')
@@ -155,18 +157,19 @@ function normalizeNodeText($node) {
155
157
  .trim();
156
158
  }
157
159
 
158
- function extractFromNodes(nodes, $) {
160
+ function removeUnwantedNodes(node) {
161
+ const unwanted = selectAll(
162
+ 'script,noscript,img,style,aside,.song_media_dropdown,.header_with_cover_art-primary_info',
163
+ node
164
+ );
165
+ unwanted.forEach((element) => DomUtils.removeElement(element));
166
+ }
167
+
168
+ function extractFromNodes(nodes) {
159
169
  const blocks = [];
160
170
  nodes.forEach((element) => {
161
- const cleaned = $(element)
162
- .clone()
163
- .find(
164
- 'script,noscript,img,style,aside,.song_media_dropdown,.header_with_cover_art-primary_info'
165
- )
166
- .remove()
167
- .end();
168
-
169
- const text = normalizeNodeText(cleaned);
171
+ removeUnwantedNodes(element);
172
+ const text = normalizeNodeText(element);
170
173
  const stripped = stripSummaryText(text);
171
174
 
172
175
  if (stripped) {
@@ -224,15 +227,18 @@ export async function fetchLyricsForGeniusSong(url) {
224
227
  }
225
228
  });
226
229
 
227
- const $ = cheerio.load(response.data);
228
- let blocks = extractFromNodes($('div[data-lyrics-container="true"]').toArray(), $);
230
+ const document = parseDocument(response.data);
231
+ let blocks = extractFromNodes(selectAll('div[data-lyrics-container="true"]', document));
229
232
 
230
233
  if (!blocks.length) {
231
- blocks = extractFromNodes($('div[class^="Lyrics__Container"]').toArray(), $);
234
+ blocks = extractFromNodes(selectAll('div[class^="Lyrics__Container"]', document));
232
235
  }
233
236
 
234
237
  if (!blocks.length) {
235
- const fallback = $('.lyrics').text().trim();
238
+ const fallback = selectAll('.lyrics', document)
239
+ .map((node) => DomUtils.textContent(node))
240
+ .join('\n')
241
+ .trim();
236
242
  if (fallback) {
237
243
  blocks.push(fallback);
238
244
  }
@@ -1,5 +1,7 @@
1
1
  import axios from 'axios';
2
- import * as cheerio from 'cheerio';
2
+ import { selectAll } from 'css-select';
3
+ import * as DomUtils from 'domutils';
4
+ import { parseDocument } from 'htmlparser2';
3
5
 
4
6
  import { normalizeLyricRecord, recomputeSyncFlags } from '../provider-result-schema.js';
5
7
  import { MELON_COOKIE, warnMissingEnv } from '../utils/config.js';
@@ -99,25 +101,30 @@ function extractSongId(value) {
99
101
  }
100
102
 
101
103
  function parseSearchPage(html) {
102
- const $ = cheerio.load(html);
104
+ const document = parseDocument(html);
103
105
  const seenIds = new Set();
104
- return $('#frm_defaultList > div > table > tbody > tr')
105
- .map((_, row) => {
106
- const $row = $(row);
107
- const cells = $row.find('td');
108
- const titleAnchor = cells.eq(2).find('a.fc_gray').first();
109
- const artistAnchor = cells.eq(3).find('#artistName > a').first();
110
- const albumAnchor = cells.eq(4).find('a').first();
111
- const titleHref = titleAnchor.attr('href') || titleAnchor.attr('onclick') || '';
112
- const artistHref = artistAnchor.attr('href') || artistAnchor.attr('onclick') || '';
106
+ return selectAll('#frm_defaultList > div > table > tbody > tr', document)
107
+ .map((row) => {
108
+ const cells = selectAll('td', row);
109
+ const titleAnchor = selectAll('a.fc_gray', cells[2] || [])[0];
110
+ const artistAnchor = selectAll('#artistName > a', cells[3] || [])[0];
111
+ const albumAnchor = selectAll('a', cells[4] || [])[0];
112
+ const titleHref =
113
+ DomUtils.getAttributeValue(titleAnchor, 'href') ||
114
+ DomUtils.getAttributeValue(titleAnchor, 'onclick') ||
115
+ '';
116
+ const artistHref =
117
+ DomUtils.getAttributeValue(artistAnchor, 'href') ||
118
+ DomUtils.getAttributeValue(artistAnchor, 'onclick') ||
119
+ '';
113
120
  let songId = extractSongId(titleHref) || extractSongId(artistHref);
114
- if (!songId) songId = extractSongId($row.html() || '');
121
+ if (!songId) songId = extractSongId(DomUtils.getOuterHTML(row) || '');
115
122
  if (!songId || seenIds.has(songId)) {
116
123
  return null;
117
124
  }
118
- const title = titleAnchor.text().trim().replace(/\s+/g, ' ');
119
- const artist = artistAnchor.text().trim().replace(/\s+/g, ' ');
120
- const album = albumAnchor.text().trim().replace(/\s+/g, ' ');
125
+ const title = DomUtils.textContent(titleAnchor).trim().replace(/\s+/g, ' ');
126
+ const artist = DomUtils.textContent(artistAnchor).trim().replace(/\s+/g, ' ');
127
+ const album = DomUtils.textContent(albumAnchor).trim().replace(/\s+/g, ' ');
121
128
  seenIds.add(songId);
122
129
  if (!title && !artist) return null;
123
130
  return { songId, title, artist, album };