mr-magic-mcp-server 0.3.12 → 0.5.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/README.md +41 -0
- package/package.json +8 -6
- package/prompts/airtable-song-importer.md +4 -1
- package/src/bin/cli.js +47 -0
- package/src/tests/run-tests.js +75 -0
- package/src/tools/cli.js +65 -1
package/README.md
CHANGED
|
@@ -401,6 +401,8 @@ The `MR_MAGIC_EXPORT_BACKEND` variable controls where formatted lyrics are store
|
|
|
401
401
|
- **`local`** (default) — writes files to `MR_MAGIC_EXPORT_DIR` (or `exports/` when
|
|
402
402
|
unset). Make sure the target directory is writable. The `export_lyrics` tool also
|
|
403
403
|
returns the raw `content` field so clients can inline results when file writes fail.
|
|
404
|
+
For global or npx CLI usage, the default `exports/` directory is relative to the
|
|
405
|
+
directory where you run `mrmagic-cli`, not the npm package install directory.
|
|
404
406
|
|
|
405
407
|
- **`inline`** — skips disk writes entirely. Each export is returned in the tool
|
|
406
408
|
response with `content` populated and `skipped: true` to signal that persistence
|
|
@@ -435,6 +437,16 @@ Or against the JSON HTTP server:
|
|
|
435
437
|
MR_MAGIC_DOWNLOAD_BASE_URL=http://127.0.0.1:3333
|
|
436
438
|
```
|
|
437
439
|
|
|
440
|
+
For global installs, start the JSON HTTP download server with the same persisted env
|
|
441
|
+
file or environment variables used for export commands:
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
mrmagic-cli server --port 3333
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Download URLs are only needed for the `redis` export backend. Local exports return
|
|
448
|
+
`file://` URLs and write files directly to `MR_MAGIC_EXPORT_DIR` or `./exports`.
|
|
449
|
+
|
|
438
450
|
## Local Deployment
|
|
439
451
|
|
|
440
452
|
Run whichever entrypoint you need via npm scripts:
|
|
@@ -1161,12 +1173,22 @@ A single CLI entrypoint (`mrmagic-cli`) is published with the package. Inside th
|
|
|
1161
1173
|
local repo use `npm run cli -- <subcommand>` unless you have run `npm link` or
|
|
1162
1174
|
installed globally.
|
|
1163
1175
|
|
|
1176
|
+
Global CLI options:
|
|
1177
|
+
|
|
1178
|
+
- `--env-path <path>` / `--env-file <path>` — load credentials from a custom `.env` file
|
|
1179
|
+
before running the command. This is useful for global installs, `npm link`, and `npx`
|
|
1180
|
+
usage where the package install directory is not your project directory.
|
|
1181
|
+
- `--save-env-path` — persist the provided `--env-path` to
|
|
1182
|
+
`~/.config/mrmagic-cli/config.json` so future `mrmagic-cli` commands load the same
|
|
1183
|
+
credentials file automatically.
|
|
1184
|
+
|
|
1164
1185
|
### Commands
|
|
1165
1186
|
|
|
1166
1187
|
| Command | Purpose | Notable flags |
|
|
1167
1188
|
| ----------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
1168
1189
|
| `mrmagic-cli search` | List candidates across providers without downloading. | `--artist`, `--title`, `--provider`, `--duration`, `--show-all`, `--pick` |
|
|
1169
1190
|
| `mrmagic-cli find` | Resolve best lyric (prefers synced) and print / export. | `--providers`, `--synced-only`, `--export`, `--format`, `--output`, `--no-romanize`, `--choose`, `--index` |
|
|
1191
|
+
| `mrmagic-cli export` | Resolve best lyric and write export files directly. | `--providers`, `--synced-only`, `--format`, `--output`, `--no-romanize` |
|
|
1170
1192
|
| `mrmagic-cli select` | Pick first match from a prioritized provider list. | `--providers`, `--artist`, `--title`, `--require-synced` |
|
|
1171
1193
|
| `mrmagic-cli server` | Start the JSON automation API. | `--host`, `--port`, `--remote` |
|
|
1172
1194
|
| `mrmagic-cli server:mcp` | Start the MCP stdio server. | — |
|
|
@@ -1180,9 +1202,28 @@ installed globally.
|
|
|
1180
1202
|
# Search all providers
|
|
1181
1203
|
npm run cli -- search --artist "BLACKPINK" --title "Kill This Love"
|
|
1182
1204
|
|
|
1205
|
+
# Global/custom install with an explicit credential file
|
|
1206
|
+
mrmagic-cli --env-path /absolute/path/to/.env find --artist "Nayeon" --title "POP!"
|
|
1207
|
+
|
|
1208
|
+
# Save that credential file path once for future global CLI commands
|
|
1209
|
+
mrmagic-cli --env-path /absolute/path/to/.env --save-env-path status
|
|
1210
|
+
|
|
1183
1211
|
# Find best lyric (prefers synced LRC)
|
|
1184
1212
|
npm run cli -- find --artist "Nayeon" --title "POP!"
|
|
1185
1213
|
|
|
1214
|
+
# Export plain text and SRT files for the best match
|
|
1215
|
+
npm run cli -- export --artist "Nayeon" --title "POP!" --format plain --format srt --output ./exports
|
|
1216
|
+
|
|
1217
|
+
# Global install: local export files are written to ./exports relative to your shell's current directory
|
|
1218
|
+
mrmagic-cli export --artist "Nayeon" --title "POP!" --format srt
|
|
1219
|
+
|
|
1220
|
+
# Global install: Redis-backed download URLs served by the JSON HTTP server
|
|
1221
|
+
MR_MAGIC_EXPORT_BACKEND=redis \
|
|
1222
|
+
MR_MAGIC_DOWNLOAD_BASE_URL=http://127.0.0.1:3333 \
|
|
1223
|
+
mrmagic-cli export --artist "Nayeon" --title "POP!" --format srt
|
|
1224
|
+
|
|
1225
|
+
mrmagic-cli server --port 3333
|
|
1226
|
+
|
|
1186
1227
|
# Pick first synced match from a prioritized provider list
|
|
1187
1228
|
npm run cli -- select --providers lrclib,genius --artist "Nayeon" --title "POP!" --require-synced
|
|
1188
1229
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mr-magic-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Lyrics MCP server connecting LRCLIB, Genius, Musixmatch, and Melon",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -61,14 +61,14 @@
|
|
|
61
61
|
"node": ">=20"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@dotenvx/dotenvx": "^1.
|
|
64
|
+
"@dotenvx/dotenvx": "^1.65.0",
|
|
65
65
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
66
|
-
"axios": "^1.
|
|
66
|
+
"axios": "^1.16.0",
|
|
67
67
|
"cheerio": "^1.2.0",
|
|
68
68
|
"commander": "^14.0.3"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"eslint": "^10.
|
|
71
|
+
"eslint": "^10.3.0",
|
|
72
72
|
"eslint-config-prettier": "^10.1.8",
|
|
73
73
|
"eslint-plugin-import-x": "^4.16.2",
|
|
74
74
|
"playwright": "^1.59.1",
|
|
@@ -85,7 +85,8 @@
|
|
|
85
85
|
"hasown": "npm:@socketregistry/hasown@^1",
|
|
86
86
|
"object-assign": "npm:@socketregistry/object-assign@^1",
|
|
87
87
|
"safer-buffer": "npm:@socketregistry/safer-buffer@^1",
|
|
88
|
-
"side-channel": "npm:@socketregistry/side-channel@^1"
|
|
88
|
+
"side-channel": "npm:@socketregistry/side-channel@^1",
|
|
89
|
+
"encoding-sniffer": "^1.0.2"
|
|
89
90
|
},
|
|
90
91
|
"resolutions": {
|
|
91
92
|
"entities": "^7.0.1",
|
|
@@ -98,6 +99,7 @@
|
|
|
98
99
|
"hasown": "npm:@socketregistry/hasown@^1",
|
|
99
100
|
"object-assign": "npm:@socketregistry/object-assign@^1",
|
|
100
101
|
"safer-buffer": "npm:@socketregistry/safer-buffer@^1",
|
|
101
|
-
"side-channel": "npm:@socketregistry/side-channel@^1"
|
|
102
|
+
"side-channel": "npm:@socketregistry/side-channel@^1",
|
|
103
|
+
"encoding-sniffer": "^1.0.2"
|
|
102
104
|
}
|
|
103
105
|
}
|
|
@@ -54,7 +54,7 @@ Examples:
|
|
|
54
54
|
- `BLACKPINK, Doja Cat, Absolutely - Crazy (Lyrics)`
|
|
55
55
|
- `Joji - Glimpse of Us (Lyrics)`
|
|
56
56
|
- `[GANG$] - Money (Remix) (Lyrics)`
|
|
57
|
-
- WRONG:`John Wick - This Song Is Lit (feat. BANKS) \\ RIGHT
|
|
57
|
+
- WRONG:`John Wick - This Song Is Lit (feat. BANKS) \\ RIGHT: `John Wick, BANKS - This Song Is Lit`
|
|
58
58
|
|
|
59
59
|
Artist names may contain brackets or special characters. Preserve them exactly.
|
|
60
60
|
|
|
@@ -85,6 +85,9 @@ Artist names may contain brackets or special characters. Preserve them exactly.
|
|
|
85
85
|
|
|
86
86
|
`build_catalog_payload` is the **required and exclusive lyric-resolution / lyric-preparation step for any Airtable entry**.
|
|
87
87
|
|
|
88
|
+
If you have an issue finding a lyric for a song with all the artists given, fallback to just using the first written artist, or the native language equivalent if found on Spotify.
|
|
89
|
+
|
|
90
|
+
If you have an issue finding a lyric for a song with the english title, use the native language version native language equivalent title of the song found from Spotify.
|
|
88
91
|
For every song that will be written to Airtable:
|
|
89
92
|
|
|
90
93
|
1. You **must** call `build_catalog_payload` before the Lyrics field can be written.
|
package/src/bin/cli.js
CHANGED
|
@@ -3,7 +3,54 @@
|
|
|
3
3
|
// Reduce structured-log noise and env-missing warnings for interactive CLI usage.
|
|
4
4
|
// These must be set before any module that reads them is evaluated, so we use
|
|
5
5
|
// a dynamic import below instead of a static one.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
6
10
|
if (!process.env.LOG_LEVEL) process.env.LOG_LEVEL = 'warn';
|
|
7
11
|
if (!process.env.MR_MAGIC_QUIET_STDIO) process.env.MR_MAGIC_QUIET_STDIO = '1';
|
|
8
12
|
|
|
13
|
+
const configPath = process.env.MR_MAGIC_CLI_CONFIG_PATH
|
|
14
|
+
? path.resolve(process.env.MR_MAGIC_CLI_CONFIG_PATH)
|
|
15
|
+
: path.join(os.homedir(), '.config', 'mrmagic-cli', 'config.json');
|
|
16
|
+
const configDir = path.dirname(configPath);
|
|
17
|
+
|
|
18
|
+
function readPersistedEnvPath() {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
21
|
+
return typeof parsed.envPath === 'string' && parsed.envPath.trim() ? parsed.envPath : null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function persistEnvPath(envPath) {
|
|
28
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
29
|
+
fs.writeFileSync(configPath, `${JSON.stringify({ envPath }, null, 2)}\n`, {
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
mode: 0o600
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const envPathFlagIndex = process.argv.findIndex(
|
|
36
|
+
(arg) => arg === '--env-path' || arg === '--env-file'
|
|
37
|
+
);
|
|
38
|
+
if (envPathFlagIndex >= 0 && process.argv[envPathFlagIndex + 1]) {
|
|
39
|
+
process.env.MR_MAGIC_ENV_PATH = process.argv[envPathFlagIndex + 1];
|
|
40
|
+
process.argv.splice(envPathFlagIndex, 2);
|
|
41
|
+
if (process.argv.includes('--save-env-path')) {
|
|
42
|
+
persistEnvPath(process.env.MR_MAGIC_ENV_PATH);
|
|
43
|
+
}
|
|
44
|
+
} else if (!process.env.MR_MAGIC_ENV_PATH) {
|
|
45
|
+
const persistedEnvPath = readPersistedEnvPath();
|
|
46
|
+
if (persistedEnvPath) {
|
|
47
|
+
process.env.MR_MAGIC_ENV_PATH = persistedEnvPath;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const saveEnvPathFlagIndex = process.argv.indexOf('--save-env-path');
|
|
52
|
+
if (saveEnvPathFlagIndex >= 0) {
|
|
53
|
+
process.argv.splice(saveEnvPathFlagIndex, 1);
|
|
54
|
+
}
|
|
55
|
+
|
|
9
56
|
await import('../tools/cli.js');
|
package/src/tests/run-tests.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
3
7
|
|
|
4
8
|
import { selectMatch } from '../index.js';
|
|
5
9
|
import { buildChooserEntries, autoPick } from '../core/find-service.js';
|
|
@@ -431,6 +435,75 @@ async function testBuildPayloadFromResultNoCacheKeyWhenNoLyrics() {
|
|
|
431
435
|
console.log('buildPayloadFromResult omits lyricsCacheKey when best has no lyrics: ok');
|
|
432
436
|
}
|
|
433
437
|
|
|
438
|
+
function testCliExportCommandHelp() {
|
|
439
|
+
const output = execFileSync(process.execPath, ['src/bin/cli.js', 'export', '--help'], {
|
|
440
|
+
encoding: 'utf8',
|
|
441
|
+
env: {
|
|
442
|
+
...process.env,
|
|
443
|
+
LOG_LEVEL: 'error',
|
|
444
|
+
MR_MAGIC_QUIET_STDIO: '1'
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
assert.ok(output.includes('Find lyrics and write plain/LRC/SRT exports'));
|
|
449
|
+
assert.ok(output.includes('--format <format>'));
|
|
450
|
+
assert.ok(output.includes('--output <dir>'));
|
|
451
|
+
|
|
452
|
+
divider();
|
|
453
|
+
console.log('CLI export command help is available: ok');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function testCliEnvPathLoadsCustomEnvFile() {
|
|
457
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mrmagic-cli-env-'));
|
|
458
|
+
const envPath = path.join(tempDir, '.env.custom');
|
|
459
|
+
const configPath = path.join(tempDir, 'config.json');
|
|
460
|
+
fs.writeFileSync(envPath, 'GENIUS_DIRECT_TOKEN=cli-env-path-token\n', 'utf8');
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
const output = execFileSync(
|
|
464
|
+
process.execPath,
|
|
465
|
+
['src/bin/cli.js', '--env-path', envPath, '--save-env-path', 'status'],
|
|
466
|
+
{
|
|
467
|
+
encoding: 'utf8',
|
|
468
|
+
env: {
|
|
469
|
+
PATH: process.env.PATH,
|
|
470
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
471
|
+
LOG_LEVEL: 'error',
|
|
472
|
+
MR_MAGIC_QUIET_STDIO: '1',
|
|
473
|
+
MR_MAGIC_CLI_CONFIG_PATH: configPath
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
assert.ok(output.includes('genius'));
|
|
479
|
+
assert.ok(output.includes('Ready'), 'custom env file should make Genius status ready');
|
|
480
|
+
|
|
481
|
+
const saved = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
482
|
+
assert.equal(saved.envPath, envPath, '--save-env-path should persist the selected env file');
|
|
483
|
+
|
|
484
|
+
const persistedOutput = execFileSync(process.execPath, ['src/bin/cli.js', 'status'], {
|
|
485
|
+
encoding: 'utf8',
|
|
486
|
+
env: {
|
|
487
|
+
PATH: process.env.PATH,
|
|
488
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
489
|
+
LOG_LEVEL: 'error',
|
|
490
|
+
MR_MAGIC_QUIET_STDIO: '1',
|
|
491
|
+
MR_MAGIC_CLI_CONFIG_PATH: configPath
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
assert.ok(
|
|
496
|
+
persistedOutput.includes('Ready'),
|
|
497
|
+
'CLI should reuse the persisted env path on later commands'
|
|
498
|
+
);
|
|
499
|
+
} finally {
|
|
500
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
divider();
|
|
504
|
+
console.log('CLI --env-path loads and persists custom env files: ok');
|
|
505
|
+
}
|
|
506
|
+
|
|
434
507
|
async function run() {
|
|
435
508
|
testAutoPickPrefersSynced();
|
|
436
509
|
testAutoPickFallbackWhenNoSynced();
|
|
@@ -447,6 +520,8 @@ async function run() {
|
|
|
447
520
|
testRomanization();
|
|
448
521
|
await testBuildPayloadFromResultReturnsCacheKey();
|
|
449
522
|
await testBuildPayloadFromResultNoCacheKeyWhenNoLyrics();
|
|
523
|
+
testCliExportCommandHelp();
|
|
524
|
+
testCliEnvPathLoadsCustomEnvFile();
|
|
450
525
|
const toolNames = mcpToolDefinitions.map((tool) => tool.name);
|
|
451
526
|
console.log('MCP tooling available:', toolNames.join(', '));
|
|
452
527
|
console.log('All sanity checks passed');
|
package/src/tools/cli.js
CHANGED
|
@@ -107,7 +107,10 @@ const program = new Command();
|
|
|
107
107
|
program
|
|
108
108
|
.name('mrmagic-cli')
|
|
109
109
|
.description('Lyrics MCP server CLI powered by LRCLIB, Genius, Musixmatch, and Melon')
|
|
110
|
-
.version('0.1.3')
|
|
110
|
+
.version('0.1.3')
|
|
111
|
+
.option('--env-path <path>', 'Path to a .env file to load before running a CLI command')
|
|
112
|
+
.option('--env-file <path>', 'Alias for --env-path')
|
|
113
|
+
.option('--save-env-path', 'Persist --env-path for future CLI commands');
|
|
111
114
|
|
|
112
115
|
function normalizeFormatOptions(value) {
|
|
113
116
|
if (!value) return [];
|
|
@@ -699,6 +702,67 @@ program
|
|
|
699
702
|
}
|
|
700
703
|
});
|
|
701
704
|
|
|
705
|
+
program
|
|
706
|
+
.command('export')
|
|
707
|
+
.description('Find lyrics and write plain/LRC/SRT exports')
|
|
708
|
+
.requiredOption('--artist <name>', 'Artist name')
|
|
709
|
+
.requiredOption('--title <name>', 'Song title')
|
|
710
|
+
.option('--album <name>', 'Album name')
|
|
711
|
+
.option('--duration <ms>', 'Track duration in milliseconds')
|
|
712
|
+
.option('--providers <list>', 'Comma-separated provider list')
|
|
713
|
+
.option('--synced-only', 'Require synced lyrics', false)
|
|
714
|
+
.option(
|
|
715
|
+
'--format <format>',
|
|
716
|
+
'Export format (plain|lrc|srt). repeatable',
|
|
717
|
+
(value, acc) => {
|
|
718
|
+
acc.push(value);
|
|
719
|
+
return acc;
|
|
720
|
+
},
|
|
721
|
+
[]
|
|
722
|
+
)
|
|
723
|
+
.option('--output <dir>', 'Directory for exports (defaults to MR_MAGIC_EXPORT_DIR or ./exports)')
|
|
724
|
+
.option('--no-romanize', 'Disable romanized lyrics', false)
|
|
725
|
+
.action(async (options) => {
|
|
726
|
+
const track = buildTrackFromOptions(options);
|
|
727
|
+
const searchLabel = [track.artist, track.title].filter(Boolean).join(' - ');
|
|
728
|
+
process.stderr.write(`Searching: ${searchLabel}...\n`);
|
|
729
|
+
const providerNames = options.providers
|
|
730
|
+
? options.providers.split(',').map((value) => value.trim())
|
|
731
|
+
: [];
|
|
732
|
+
const result = await runFind(track, {
|
|
733
|
+
providerNames,
|
|
734
|
+
syncedOnly: options.syncedOnly
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
if (!result.best) {
|
|
738
|
+
console.error('No best match available');
|
|
739
|
+
process.exitCode = 1;
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const includeRomanization = options.noRomanize ? false : true;
|
|
744
|
+
const exports = await exportLyrics(result.best, {
|
|
745
|
+
formats: deriveFormatSet(options.format),
|
|
746
|
+
output: options.output,
|
|
747
|
+
includeRomanization
|
|
748
|
+
});
|
|
749
|
+
console.log(
|
|
750
|
+
JSON.stringify(
|
|
751
|
+
{
|
|
752
|
+
picked: {
|
|
753
|
+
provider: result.best.provider || 'unknown',
|
|
754
|
+
synced: Boolean(result.best.synced),
|
|
755
|
+
artist: result.best.artist || track.artist || 'unknown',
|
|
756
|
+
title: result.best.title || track.title || 'song'
|
|
757
|
+
},
|
|
758
|
+
exports
|
|
759
|
+
},
|
|
760
|
+
null,
|
|
761
|
+
2
|
|
762
|
+
)
|
|
763
|
+
);
|
|
764
|
+
});
|
|
765
|
+
|
|
702
766
|
program
|
|
703
767
|
.command('search-provider')
|
|
704
768
|
.description('Search a specific provider only')
|