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
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
|
|
3
3
|
import { runFind, runSearch, runProviderSearch } from '../core/find-service.js';
|
|
4
|
-
import {
|
|
4
|
+
import { extractPlainPreview, extractSyncedPreview } from '../core/preview.js';
|
|
5
|
+
import { buildProviderReferenceFingerprint, lyricContentScore } from '../provider-result-schema.js';
|
|
5
6
|
import {
|
|
6
7
|
buildActionContext,
|
|
7
8
|
buildPayloadFromResult,
|
|
8
9
|
buildCatalogPayload,
|
|
10
|
+
buildCatalogPayloadFromResult,
|
|
9
11
|
formatRecord,
|
|
10
12
|
catalogCache,
|
|
11
13
|
catalogCacheKey
|
|
12
14
|
} from '../services/lyrics-service.js';
|
|
13
15
|
import { pushCatalogToAirtable } from '../services/airtable-writer.js';
|
|
14
|
-
import { getProviderStatus } from '../index.js';
|
|
16
|
+
import { getProviderStatus, resolveProviderReference } from '../index.js';
|
|
15
17
|
|
|
16
18
|
const trackSchema = {
|
|
17
19
|
type: 'object',
|
|
@@ -120,22 +122,56 @@ const selectCriteriaSchema = {
|
|
|
120
122
|
additionalProperties: false
|
|
121
123
|
};
|
|
122
124
|
|
|
123
|
-
const
|
|
125
|
+
const providerReferenceSchema = {
|
|
124
126
|
type: 'object',
|
|
125
|
-
description: '
|
|
127
|
+
description: 'Compact provider reference returned by MCP search tools for exact recall.',
|
|
126
128
|
properties: {
|
|
127
129
|
provider: { type: 'string', description: 'Provider slug for the result.' },
|
|
128
|
-
providerId: {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
providerId: {
|
|
131
|
+
type: ['string', 'null'],
|
|
132
|
+
description: 'Provider-specific identifier if available.'
|
|
133
|
+
},
|
|
134
|
+
ids: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
description: 'Optional provider-specific identifiers such as Melon songId.',
|
|
137
|
+
additionalProperties: { type: 'string' }
|
|
138
|
+
},
|
|
139
|
+
title: { type: ['string', 'null'], description: 'Track title for provider recall.' },
|
|
140
|
+
artist: { type: ['string', 'null'], description: 'Artist name for provider recall.' },
|
|
141
|
+
album: { type: ['string', 'null'], description: 'Album name for provider recall.' },
|
|
132
142
|
duration: {
|
|
133
|
-
type: 'string',
|
|
143
|
+
type: ['number', 'string', 'null'],
|
|
144
|
+
description: 'Track duration hint used when replaying provider lookups.'
|
|
145
|
+
},
|
|
146
|
+
sourceUrl: {
|
|
147
|
+
type: ['string', 'null'],
|
|
148
|
+
description: 'Canonical source URL for exact-result recall when available.'
|
|
149
|
+
},
|
|
150
|
+
fingerprint: {
|
|
151
|
+
type: ['string', 'null'],
|
|
152
|
+
description: 'Stable fingerprint for recalling preview-only matches without provider IDs.'
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
additionalProperties: false
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const compactSearchResultSchema = {
|
|
159
|
+
type: 'object',
|
|
160
|
+
description: 'Compact MCP search result preview. Search tools never return full lyrics.',
|
|
161
|
+
properties: {
|
|
162
|
+
provider: { type: 'string', description: 'Provider slug for the result.' },
|
|
163
|
+
providerId: {
|
|
164
|
+
type: ['string', 'null'],
|
|
165
|
+
description: 'Provider-specific identifier if available.'
|
|
166
|
+
},
|
|
167
|
+
title: { type: ['string', 'null'], description: 'Track title from the provider result.' },
|
|
168
|
+
artist: { type: ['string', 'null'], description: 'Artist name from the provider result.' },
|
|
169
|
+
album: { type: ['string', 'null'], description: 'Album name if provided.' },
|
|
170
|
+
duration: {
|
|
171
|
+
type: ['number', 'null'],
|
|
134
172
|
description: 'Duration (seconds) if reported.'
|
|
135
173
|
},
|
|
136
|
-
|
|
137
|
-
syncedLyrics: { type: 'string', description: 'Synced lyric text if hydrated.' },
|
|
138
|
-
sourceUrl: { type: 'string', description: 'Canonical URL to view the lyrics.' },
|
|
174
|
+
sourceUrl: { type: ['string', 'null'], description: 'Canonical URL to view the lyrics.' },
|
|
139
175
|
confidence: {
|
|
140
176
|
type: 'number',
|
|
141
177
|
description: 'Confidence score for the match (0-1 scale when available).'
|
|
@@ -150,10 +186,19 @@ const normalizedLyricRecordSchema = {
|
|
|
150
186
|
description: 'Number of timestamped lines detected in synced lyrics.'
|
|
151
187
|
},
|
|
152
188
|
status: { type: ['string', 'null'], description: 'Provider-specific status for the record.' },
|
|
153
|
-
|
|
154
|
-
type:
|
|
155
|
-
description: '
|
|
156
|
-
}
|
|
189
|
+
hasLyrics: {
|
|
190
|
+
type: 'boolean',
|
|
191
|
+
description: 'True when the provider already returned lyric text.'
|
|
192
|
+
},
|
|
193
|
+
plainPreview: {
|
|
194
|
+
type: ['string', 'null'],
|
|
195
|
+
description: 'Short preview of plain lyrics when available.'
|
|
196
|
+
},
|
|
197
|
+
syncedPreview: {
|
|
198
|
+
type: ['string', 'null'],
|
|
199
|
+
description: 'Short preview of synced lyrics when available.'
|
|
200
|
+
},
|
|
201
|
+
reference: providerReferenceSchema
|
|
157
202
|
},
|
|
158
203
|
additionalProperties: false
|
|
159
204
|
};
|
|
@@ -163,44 +208,214 @@ const matchSchema = {
|
|
|
163
208
|
description: 'A single search result entry (provider + normalized lyric record).',
|
|
164
209
|
properties: {
|
|
165
210
|
provider: { type: 'string', description: 'Provider slug for the result.' },
|
|
166
|
-
result:
|
|
211
|
+
result: compactSearchResultSchema
|
|
167
212
|
},
|
|
168
213
|
additionalProperties: false
|
|
169
214
|
};
|
|
170
215
|
|
|
216
|
+
const searchGroupSchema = {
|
|
217
|
+
type: 'object',
|
|
218
|
+
description: 'Provider bucket returned by search_lyrics with preview-only results.',
|
|
219
|
+
properties: {
|
|
220
|
+
provider: { type: 'string', description: 'Provider slug for this bucket.' },
|
|
221
|
+
results: {
|
|
222
|
+
type: 'array',
|
|
223
|
+
description: 'Compact preview-only matches for the provider.',
|
|
224
|
+
items: compactSearchResultSchema
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
additionalProperties: false
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
function compactStringMap(input = {}) {
|
|
231
|
+
const entries = Object.entries(input).filter(
|
|
232
|
+
([, value]) => value !== null && value !== undefined && value !== ''
|
|
233
|
+
);
|
|
234
|
+
if (entries.length === 0) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
return Object.fromEntries(entries.map(([key, value]) => [key, value.toString()]));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function buildProviderReference(record = {}) {
|
|
241
|
+
const reference = {
|
|
242
|
+
provider: record.provider,
|
|
243
|
+
providerId: record.providerId ?? null,
|
|
244
|
+
title: record.title ?? null,
|
|
245
|
+
artist: record.artist ?? null,
|
|
246
|
+
album: record.album ?? null,
|
|
247
|
+
duration: record.duration ?? null,
|
|
248
|
+
sourceUrl: record.sourceUrl ?? null
|
|
249
|
+
};
|
|
250
|
+
const ids = compactStringMap(record.ids || {});
|
|
251
|
+
if (ids) {
|
|
252
|
+
reference.ids = ids;
|
|
253
|
+
}
|
|
254
|
+
reference.fingerprint = buildProviderReferenceFingerprint(record);
|
|
255
|
+
return reference;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function buildCompactSearchResult(record = {}) {
|
|
259
|
+
return {
|
|
260
|
+
provider: record.provider,
|
|
261
|
+
providerId: record.providerId ?? null,
|
|
262
|
+
title: record.title ?? null,
|
|
263
|
+
artist: record.artist ?? null,
|
|
264
|
+
album: record.album ?? null,
|
|
265
|
+
duration: record.duration ?? null,
|
|
266
|
+
sourceUrl: record.sourceUrl ?? null,
|
|
267
|
+
confidence: record.confidence ?? 0,
|
|
268
|
+
synced: Boolean(record.synced),
|
|
269
|
+
plainOnly: Boolean(record.plainOnly),
|
|
270
|
+
timestampCount: record.timestampCount ?? 0,
|
|
271
|
+
status: record.status ?? null,
|
|
272
|
+
hasLyrics: Boolean(record.plainLyrics || record.syncedLyrics),
|
|
273
|
+
plainPreview: extractPlainPreview(record) || null,
|
|
274
|
+
syncedPreview: extractSyncedPreview(record) || null,
|
|
275
|
+
reference: buildProviderReference(record)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function projectSearchGroups(groups = []) {
|
|
280
|
+
return groups.map((group) => ({
|
|
281
|
+
provider: group.provider,
|
|
282
|
+
results: Array.isArray(group.results)
|
|
283
|
+
? group.results.map((record) => buildCompactSearchResult(record))
|
|
284
|
+
: []
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function normalizeMatchInput(match) {
|
|
289
|
+
if (!match || typeof match !== 'object') {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (match.result && typeof match.result === 'object') {
|
|
294
|
+
return {
|
|
295
|
+
provider: match.provider || match.result.provider || match.result.reference?.provider || null,
|
|
296
|
+
result: match.result
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (match.reference || match.providerId || match.title || match.artist) {
|
|
301
|
+
return {
|
|
302
|
+
provider: match.provider || match.reference?.provider || null,
|
|
303
|
+
result: match
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function flattenSelectableMatches(args = {}) {
|
|
311
|
+
const groupedItems = Array.isArray(args.items) ? args.items : [];
|
|
312
|
+
const groupedMatches = groupedItems.flatMap((item) =>
|
|
313
|
+
(item.results || []).map((result) => ({ provider: item.provider || result.provider, result }))
|
|
314
|
+
);
|
|
315
|
+
const directMatches = Array.isArray(args.matches)
|
|
316
|
+
? args.matches.map((entry) => normalizeMatchInput(entry)).filter(Boolean)
|
|
317
|
+
: [];
|
|
318
|
+
return [...groupedMatches, ...directMatches];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function mergeTrackWithResult(track = {}, result = {}) {
|
|
322
|
+
return {
|
|
323
|
+
title: track.title || result.title || '',
|
|
324
|
+
artist: track.artist || result.artist || '',
|
|
325
|
+
album: track.album || result.album || null,
|
|
326
|
+
duration: track.duration ?? result.duration ?? null
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function resolveLookupInputs(args = {}) {
|
|
331
|
+
const normalizedMatch = normalizeMatchInput(args.match);
|
|
332
|
+
const result = normalizedMatch?.result || {};
|
|
333
|
+
const reference = args.reference || result.reference || null;
|
|
334
|
+
const track = mergeTrackWithResult(args.track || {}, result);
|
|
335
|
+
return { track, reference };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function assertLookupInputs(track, reference) {
|
|
339
|
+
if (reference) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (track?.title || track?.artist) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
throw new McpError(
|
|
348
|
+
ErrorCode.InvalidParams,
|
|
349
|
+
'Provide track metadata or a provider reference from search_lyrics/search_provider'
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function resolveFindResult(args, options = {}, { syncedOnly = false } = {}) {
|
|
354
|
+
const { track, reference } = resolveLookupInputs(args);
|
|
355
|
+
assertLookupInputs(track, reference);
|
|
356
|
+
|
|
357
|
+
if (reference) {
|
|
358
|
+
const resolved = await resolveProviderReference(reference, track);
|
|
359
|
+
return buildResolvedReferenceResult(resolved, { syncedOnly });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return runFind(track, { ...options, syncedOnly });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function buildResolvedReferenceResult(resolved, { syncedOnly = false } = {}) {
|
|
366
|
+
if (!resolved || lyricContentScore(resolved) <= 0) {
|
|
367
|
+
return { matches: [], best: null };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (syncedOnly && !resolved.synced) {
|
|
371
|
+
return { matches: [], best: null };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
matches: [{ provider: resolved.provider, result: resolved }],
|
|
376
|
+
best: resolved
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
171
380
|
export const mcpToolDefinitions = [
|
|
172
381
|
{
|
|
173
382
|
name: 'find_lyrics',
|
|
174
383
|
description:
|
|
175
|
-
'Find the best lyric match across providers (prefers synced when available). ' +
|
|
384
|
+
'Find the best lyric match across providers (prefers synced when available), or resolve an exact provider reference returned by the MCP search tools. ' +
|
|
176
385
|
'Returns lyricsCacheKey when lyrics are resolved — pass this to push_catalog_to_airtable ' +
|
|
177
386
|
'without calling build_catalog_payload first.',
|
|
178
387
|
inputSchema: {
|
|
179
388
|
type: 'object',
|
|
180
|
-
description:
|
|
389
|
+
description:
|
|
390
|
+
'Provide track metadata, or pass a compact provider reference / selected search result from search_lyrics or search_provider.',
|
|
181
391
|
properties: {
|
|
182
392
|
track: trackSchema,
|
|
393
|
+
reference: providerReferenceSchema,
|
|
394
|
+
match: matchSchema,
|
|
183
395
|
options: {
|
|
184
396
|
type: 'object',
|
|
185
397
|
description: 'Optional provider hints or overrides.',
|
|
186
398
|
additionalProperties: false
|
|
187
399
|
}
|
|
188
400
|
},
|
|
189
|
-
|
|
401
|
+
additionalProperties: false
|
|
190
402
|
}
|
|
191
403
|
},
|
|
192
404
|
{
|
|
193
405
|
name: 'build_catalog_payload',
|
|
194
406
|
description:
|
|
195
|
-
'Return a compact payload suitable for Airtable inserts/exports. For large lyrics, send object args and use omitInlineLyrics + lyricsPayloadMode to avoid JSON truncation in downstream automations. Airtable-safe compact mode can auto-promote payload transport to reference.',
|
|
407
|
+
'Return a compact payload suitable for Airtable inserts/exports. Accepts track metadata or an exact provider reference from the MCP search tools. For large lyrics, send object args and use omitInlineLyrics + lyricsPayloadMode to avoid JSON truncation in downstream automations. Airtable-safe compact mode can auto-promote payload transport to reference.',
|
|
196
408
|
inputSchema: {
|
|
197
409
|
type: 'object',
|
|
198
|
-
description:
|
|
410
|
+
description:
|
|
411
|
+
'Provide a track, or pass a compact provider reference / selected search result plus optional catalog preferences.',
|
|
199
412
|
properties: {
|
|
200
413
|
track: trackSchema,
|
|
414
|
+
reference: providerReferenceSchema,
|
|
415
|
+
match: matchSchema,
|
|
201
416
|
options: catalogOptionsSchema
|
|
202
417
|
},
|
|
203
|
-
|
|
418
|
+
additionalProperties: false
|
|
204
419
|
}
|
|
205
420
|
},
|
|
206
421
|
{
|
|
@@ -211,24 +426,28 @@ export const mcpToolDefinitions = [
|
|
|
211
426
|
inputSchema: {
|
|
212
427
|
type: 'object',
|
|
213
428
|
description:
|
|
214
|
-
'Provide a
|
|
429
|
+
'Provide track metadata, or pass a compact provider reference / selected search result to look up synced lyrics only.',
|
|
215
430
|
properties: {
|
|
216
431
|
track: trackSchema,
|
|
432
|
+
reference: providerReferenceSchema,
|
|
433
|
+
match: matchSchema,
|
|
217
434
|
options: {
|
|
218
435
|
type: 'object',
|
|
219
436
|
description: 'Optional provider hints or overrides.',
|
|
220
437
|
additionalProperties: false
|
|
221
438
|
}
|
|
222
439
|
},
|
|
223
|
-
|
|
440
|
+
additionalProperties: false
|
|
224
441
|
}
|
|
225
442
|
},
|
|
226
443
|
{
|
|
227
444
|
name: 'search_lyrics',
|
|
228
|
-
description:
|
|
445
|
+
description:
|
|
446
|
+
'List preview-only candidate matches from every provider. MCP search results never include full lyrics or raw provider payloads.',
|
|
229
447
|
inputSchema: {
|
|
230
448
|
type: 'object',
|
|
231
|
-
description:
|
|
449
|
+
description:
|
|
450
|
+
'Provide the basic track metadata to retrieve preview-only matches and reusable provider references.',
|
|
232
451
|
properties: {
|
|
233
452
|
track: trackSchema
|
|
234
453
|
},
|
|
@@ -237,10 +456,12 @@ export const mcpToolDefinitions = [
|
|
|
237
456
|
},
|
|
238
457
|
{
|
|
239
458
|
name: 'search_provider',
|
|
240
|
-
description:
|
|
459
|
+
description:
|
|
460
|
+
'Search a single provider (e.g., LRCLIB, Genius, Melon) for preview-only potential matches. MCP search results never include full lyrics or raw provider payloads.',
|
|
241
461
|
inputSchema: {
|
|
242
462
|
type: 'object',
|
|
243
|
-
description:
|
|
463
|
+
description:
|
|
464
|
+
'Provide a track plus a provider slug to limit the search scope and return reusable provider references.',
|
|
244
465
|
properties: {
|
|
245
466
|
provider: {
|
|
246
467
|
type: 'string',
|
|
@@ -259,16 +480,19 @@ export const mcpToolDefinitions = [
|
|
|
259
480
|
{
|
|
260
481
|
name: 'export_lyrics',
|
|
261
482
|
description:
|
|
262
|
-
'Find lyrics and save plain/LRC/SRT plus romanized variants to disk. ' +
|
|
483
|
+
'Find lyrics and save plain/LRC/SRT plus romanized variants to disk, or resolve an exact provider reference from MCP search tools. ' +
|
|
263
484
|
'Also returns lyricsCacheKey so push_catalog_to_airtable can be called immediately.',
|
|
264
485
|
inputSchema: {
|
|
265
486
|
type: 'object',
|
|
266
|
-
description:
|
|
487
|
+
description:
|
|
488
|
+
'Provide the track, or pass a compact provider reference / selected search result and export options to write files to disk.',
|
|
267
489
|
properties: {
|
|
268
490
|
track: trackSchema,
|
|
491
|
+
reference: providerReferenceSchema,
|
|
492
|
+
match: matchSchema,
|
|
269
493
|
options: exportOptionsSchema
|
|
270
494
|
},
|
|
271
|
-
|
|
495
|
+
additionalProperties: false
|
|
272
496
|
}
|
|
273
497
|
},
|
|
274
498
|
{
|
|
@@ -281,9 +505,11 @@ export const mcpToolDefinitions = [
|
|
|
281
505
|
description: 'Provide the track and formatting options for in-memory rendering.',
|
|
282
506
|
properties: {
|
|
283
507
|
track: trackSchema,
|
|
508
|
+
reference: providerReferenceSchema,
|
|
509
|
+
match: matchSchema,
|
|
284
510
|
options: formatOptionsSchema
|
|
285
511
|
},
|
|
286
|
-
|
|
512
|
+
additionalProperties: false
|
|
287
513
|
}
|
|
288
514
|
},
|
|
289
515
|
{
|
|
@@ -295,12 +521,22 @@ export const mcpToolDefinitions = [
|
|
|
295
521
|
description:
|
|
296
522
|
'Select a single result either by passing matches + criteria or by supplying match directly.',
|
|
297
523
|
properties: {
|
|
524
|
+
items: {
|
|
525
|
+
type: 'array',
|
|
526
|
+
description: 'Grouped results returned from search_lyrics.',
|
|
527
|
+
items: searchGroupSchema
|
|
528
|
+
},
|
|
298
529
|
matches: {
|
|
299
530
|
type: 'array',
|
|
300
|
-
description:
|
|
301
|
-
|
|
531
|
+
description:
|
|
532
|
+
'Results returned from search_provider, or flattened match entries from previous search output.',
|
|
533
|
+
items: {
|
|
534
|
+
anyOf: [matchSchema, compactSearchResultSchema]
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
match: {
|
|
538
|
+
anyOf: [matchSchema, compactSearchResultSchema]
|
|
302
539
|
},
|
|
303
|
-
match: matchSchema,
|
|
304
540
|
criteria: selectCriteriaSchema
|
|
305
541
|
}
|
|
306
542
|
}
|
|
@@ -370,28 +606,33 @@ export const mcpToolDefinitions = [
|
|
|
370
606
|
];
|
|
371
607
|
|
|
372
608
|
export async function handleMcpTool(name, args = {}) {
|
|
373
|
-
const track = args.track || {};
|
|
374
609
|
const options = args.options || {};
|
|
375
610
|
|
|
376
611
|
if (name === 'find_lyrics') {
|
|
377
|
-
const result = await
|
|
612
|
+
const result = await resolveFindResult(args, options);
|
|
378
613
|
return buildPayloadFromResult(result, buildActionContext(options));
|
|
379
614
|
}
|
|
380
615
|
|
|
381
616
|
if (name === 'find_synced_lyrics') {
|
|
382
|
-
const result = await
|
|
617
|
+
const result = await resolveFindResult(args, options, { syncedOnly: true });
|
|
383
618
|
return buildPayloadFromResult(result, buildActionContext(options));
|
|
384
619
|
}
|
|
385
620
|
|
|
386
621
|
if (name === 'search_lyrics') {
|
|
387
|
-
|
|
622
|
+
const { track } = resolveLookupInputs(args);
|
|
623
|
+
assertLookupInputs(track, null);
|
|
624
|
+
return projectSearchGroups(await runSearch(track));
|
|
388
625
|
}
|
|
389
626
|
|
|
390
627
|
if (name === 'search_provider') {
|
|
391
628
|
if (!args.provider) {
|
|
392
629
|
throw new McpError(ErrorCode.InvalidParams, 'provider is required');
|
|
393
630
|
}
|
|
394
|
-
|
|
631
|
+
const { track } = resolveLookupInputs(args);
|
|
632
|
+
assertLookupInputs(track, null);
|
|
633
|
+
return (await runProviderSearch(args.provider, track)).map((record) =>
|
|
634
|
+
buildCompactSearchResult(record)
|
|
635
|
+
);
|
|
395
636
|
}
|
|
396
637
|
|
|
397
638
|
if (name === 'get_provider_status') {
|
|
@@ -399,7 +640,7 @@ export async function handleMcpTool(name, args = {}) {
|
|
|
399
640
|
}
|
|
400
641
|
|
|
401
642
|
if (name === 'export_lyrics') {
|
|
402
|
-
const result = await
|
|
643
|
+
const result = await resolveFindResult(args, options);
|
|
403
644
|
const context = buildActionContext({ ...options, export: true });
|
|
404
645
|
const payload = await buildPayloadFromResult(result, context);
|
|
405
646
|
// buildPayloadFromResult already calls exportBestResult internally when
|
|
@@ -408,7 +649,7 @@ export async function handleMcpTool(name, args = {}) {
|
|
|
408
649
|
}
|
|
409
650
|
|
|
410
651
|
if (name === 'format_lyrics') {
|
|
411
|
-
const result = await
|
|
652
|
+
const result = await resolveFindResult(args, options);
|
|
412
653
|
const best = result?.best;
|
|
413
654
|
if (!best) {
|
|
414
655
|
return { error: 'No match found' };
|
|
@@ -433,24 +674,28 @@ export async function handleMcpTool(name, args = {}) {
|
|
|
433
674
|
}
|
|
434
675
|
|
|
435
676
|
if (name === 'build_catalog_payload') {
|
|
436
|
-
|
|
677
|
+
const { track, reference } = resolveLookupInputs(args);
|
|
678
|
+
assertLookupInputs(track, reference);
|
|
679
|
+
|
|
680
|
+
if (!reference) {
|
|
681
|
+
return buildCatalogPayload(track, options);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const resolved = await resolveProviderReference(reference, track);
|
|
685
|
+
const { best } = buildResolvedReferenceResult(resolved);
|
|
686
|
+
return buildCatalogPayloadFromResult(best, track, options);
|
|
437
687
|
}
|
|
438
688
|
|
|
439
689
|
if (name === 'select_match') {
|
|
440
690
|
if (args.match) {
|
|
441
|
-
return args.match;
|
|
691
|
+
return normalizeMatchInput(args.match) ?? args.match;
|
|
442
692
|
}
|
|
443
|
-
const matches =
|
|
693
|
+
const matches = flattenSelectableMatches(args);
|
|
444
694
|
if (matches.length === 0) {
|
|
445
695
|
return { error: 'No matches provided' };
|
|
446
696
|
}
|
|
447
697
|
const { provider, requireSynced, index } = args.criteria || {};
|
|
448
|
-
|
|
449
|
-
// so the caller never accidentally selects an empty result.
|
|
450
|
-
let filtered = matches.filter((entry) => lyricContentScore(entry.result) > 0);
|
|
451
|
-
if (filtered.length === 0) {
|
|
452
|
-
return { error: 'No matches with lyric content found' };
|
|
453
|
-
}
|
|
698
|
+
let filtered = matches;
|
|
454
699
|
if (provider) {
|
|
455
700
|
filtered = filtered.filter((entry) => entry.provider === provider);
|
|
456
701
|
}
|