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.
- package/.env.example +36 -26
- package/README.md +166 -13
- package/package.json +9 -7
- package/prompts/airtable-song-importer.md +143 -34
- package/src/index.js +89 -3
- package/src/provider-result-schema.js +88 -0
- package/src/providers/lrclib.js +25 -6
- package/src/providers/melon.js +46 -10
- package/src/services/lyrics-service.js +4 -0
- package/src/tests/mcp-tools.test.js +157 -2
- package/src/transport/mcp-tools.js +297 -52
|
@@ -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 {
|
|
4
|
-
|
|
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();
|