mr-magic-mcp-server 0.3.9 → 0.3.11

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.
@@ -95,6 +95,10 @@ export async function buildCatalogPayload(track, actionOptions = {}) {
95
95
  return buildCatalogResponse(result, track, actionOptions);
96
96
  }
97
97
 
98
+ export async function buildCatalogPayloadFromResult(best, requestedTrack = {}, actionOptions = {}) {
99
+ return buildCatalogResponse({ best }, requestedTrack, actionOptions);
100
+ }
101
+
98
102
  export async function exportBestResult(result, context) {
99
103
  if (!context.shouldExport || !result?.best) return null;
100
104
  return exportLyrics(result.best, {
@@ -1,7 +1,11 @@
1
1
  import assert from 'node:assert/strict';
2
2
 
3
- import { mcpToolDefinitions, handleMcpTool } from '../src/transport/mcp-tools.js';
4
- import { buildMcpResponse } from '../src/transport/mcp-response.js';
3
+ import {
4
+ mcpToolDefinitions,
5
+ handleMcpTool,
6
+ buildResolvedReferenceResult
7
+ } from '../transport/mcp-tools.js';
8
+ import { buildMcpResponse } from '../transport/mcp-response.js';
5
9
 
6
10
  const sampleTrack = {
7
11
  title: 'Kill This Love',
@@ -60,6 +64,150 @@ async function testSearchProviderReturnsArray() {
60
64
  track: sampleTrack
61
65
  });
62
66
  assert.ok(Array.isArray(results));
67
+ const firstResult = results[0];
68
+ if (firstResult) {
69
+ assert.ok(!Object.prototype.hasOwnProperty.call(firstResult, 'plainLyrics'));
70
+ assert.ok(!Object.prototype.hasOwnProperty.call(firstResult, 'syncedLyrics'));
71
+ assert.ok(!Object.prototype.hasOwnProperty.call(firstResult, 'rawRecord'));
72
+ assert.equal(firstResult.reference?.provider, 'lrclib');
73
+ }
74
+ }
75
+
76
+ async function testSearchLyricsReturnsPreviewOnlyGroups() {
77
+ const groups = await handleMcpTool('search_lyrics', { track: sampleTrack });
78
+ assert.ok(Array.isArray(groups), 'search_lyrics should return provider groups');
79
+ const firstGroup = groups.find(
80
+ (group) => Array.isArray(group.results) && group.results.length > 0
81
+ );
82
+ if (firstGroup) {
83
+ const firstResult = firstGroup.results[0];
84
+ assert.ok(firstResult.reference?.provider, 'preview result should include provider reference');
85
+ assert.ok(!Object.prototype.hasOwnProperty.call(firstResult, 'plainLyrics'));
86
+ assert.ok(!Object.prototype.hasOwnProperty.call(firstResult, 'syncedLyrics'));
87
+ assert.ok(!Object.prototype.hasOwnProperty.call(firstResult, 'rawRecord'));
88
+ }
89
+ }
90
+
91
+ async function testSelectMatchAcceptsGroupedSearchResults() {
92
+ const response = await handleMcpTool('select_match', {
93
+ items: [
94
+ {
95
+ provider: 'melon',
96
+ results: [
97
+ {
98
+ provider: 'melon',
99
+ providerId: '38914304',
100
+ title: 'Summer Nights',
101
+ artist: 'St. Lucia',
102
+ synced: false,
103
+ reference: { provider: 'melon', providerId: '38914304', ids: { songId: '38914304' } }
104
+ }
105
+ ]
106
+ },
107
+ {
108
+ provider: 'lrclib',
109
+ results: [
110
+ {
111
+ provider: 'lrclib',
112
+ providerId: '25265938',
113
+ title: 'Summer Nights',
114
+ artist: 'St. Lucia',
115
+ synced: true,
116
+ reference: { provider: 'lrclib', providerId: '25265938', ids: { trackId: '25265938' } }
117
+ }
118
+ ]
119
+ }
120
+ ],
121
+ criteria: { provider: 'lrclib', requireSynced: true }
122
+ });
123
+ assert.equal(response?.result?.providerId, '25265938');
124
+ }
125
+
126
+ async function testFindLyricsAcceptsSelectedMatch() {
127
+ const results = await handleMcpTool('search_provider', {
128
+ provider: 'lrclib',
129
+ track: sampleTrack
130
+ });
131
+ const selected = results[0];
132
+ if (!selected) {
133
+ return;
134
+ }
135
+ const response = await handleMcpTool('find_lyrics', { match: selected });
136
+ assert.ok(response?.best, 'find_lyrics should resolve a selected search result');
137
+ assert.equal(response.best.providerId, selected.providerId);
138
+ }
139
+
140
+ async function testFindLyricsAcceptsReferenceOnlySelection() {
141
+ const results = await handleMcpTool('search_provider', {
142
+ provider: 'lrclib',
143
+ track: sampleTrack
144
+ });
145
+ const selected = results[0];
146
+ if (!selected?.reference) {
147
+ return;
148
+ }
149
+
150
+ const response = await handleMcpTool('find_lyrics', { reference: selected.reference });
151
+ assert.ok(response?.best, 'find_lyrics should resolve a bare provider reference');
152
+ assert.equal(response.best.providerId, selected.providerId);
153
+ }
154
+
155
+ async function testBuildCatalogPayloadAcceptsReferenceOnlySelection() {
156
+ const results = await handleMcpTool('search_provider', {
157
+ provider: 'lrclib',
158
+ track: sampleTrack
159
+ });
160
+ const selected = results[0];
161
+ if (!selected?.reference) {
162
+ return;
163
+ }
164
+
165
+ const response = await handleMcpTool('build_catalog_payload', {
166
+ reference: selected.reference,
167
+ options: { preferRomanized: false }
168
+ });
169
+ assert.ok(response?.provider, 'catalog payload should resolve from a bare provider reference');
170
+ assert.equal(response.providerId, selected.providerId);
171
+ }
172
+
173
+ async function testResolvedMelonReferenceWithoutLyricsIsRejected() {
174
+ const resolved = {
175
+ provider: 'melon',
176
+ providerId: '38914304',
177
+ title: 'Summer Nights',
178
+ artist: 'St. Lucia',
179
+ plainLyrics: null,
180
+ syncedLyrics: null,
181
+ synced: false
182
+ };
183
+
184
+ const result = buildResolvedReferenceResult(resolved);
185
+ assert.equal(result.best, null, 'empty Melon reference hydration should not become best');
186
+ assert.deepEqual(
187
+ result.matches,
188
+ [],
189
+ 'empty Melon reference hydration should not produce matches'
190
+ );
191
+ }
192
+
193
+ async function testResolvedGeniusReferenceWithoutLyricsIsRejected() {
194
+ const resolved = {
195
+ provider: 'genius',
196
+ providerId: '12345',
197
+ title: 'Song',
198
+ artist: 'Artist',
199
+ plainLyrics: ' ',
200
+ syncedLyrics: null,
201
+ synced: false
202
+ };
203
+
204
+ const result = buildResolvedReferenceResult(resolved);
205
+ assert.equal(result.best, null, 'empty Genius reference hydration should not become best');
206
+ assert.deepEqual(
207
+ result.matches,
208
+ [],
209
+ 'empty Genius reference hydration should not produce matches'
210
+ );
63
211
  }
64
212
 
65
213
  async function testFormatLyricsShape() {
@@ -233,6 +381,13 @@ async function run() {
233
381
  await testFindSyncedLyricsTool();
234
382
  await testSearchProviderRequiresProvider();
235
383
  await testSearchProviderReturnsArray();
384
+ await testSearchLyricsReturnsPreviewOnlyGroups();
385
+ await testSelectMatchAcceptsGroupedSearchResults();
386
+ await testFindLyricsAcceptsSelectedMatch();
387
+ await testFindLyricsAcceptsReferenceOnlySelection();
388
+ await testBuildCatalogPayloadAcceptsReferenceOnlySelection();
389
+ await testResolvedMelonReferenceWithoutLyricsIsRejected();
390
+ await testResolvedGeniusReferenceWithoutLyricsIsRejected();
236
391
  await testFormatLyricsShape();
237
392
  await testBuildCatalogPayload();
238
393
  await testBuildCatalogPayloadWithLyricsPayload();