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 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.12",
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.61.1",
64
+ "@dotenvx/dotenvx": "^1.65.0",
65
65
  "@modelcontextprotocol/sdk": "^1.29.0",
66
- "axios": "^1.15.0",
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.2.1",
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:`John Wick, BANKS - This Song Is Lit`
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');
@@ -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')