mr-magic-mcp-server 0.2.6 → 0.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-magic-mcp-server",
3
- "version": "0.2.6",
3
+ "version": "0.3.1",
4
4
  "description": "Lyrics MCP server connecting LRCLIB, Genius, Musixmatch, and Melon",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -21,6 +21,7 @@
21
21
  ".env.example"
22
22
  ],
23
23
  "scripts": {
24
+ "cleanup": "eslint . --fix && prettier --write .",
24
25
  "cli": "node src/bin/cli.js",
25
26
  "server:http": "node src/bin/http-server.js",
26
27
  "server:mcp": "node src/bin/mcp-server.js",
@@ -30,7 +31,8 @@
30
31
  "format": "prettier --write .",
31
32
  "format:check": "prettier --check .",
32
33
  "test": "node src/tests/run-tests.js",
33
- "fetch:musixmatch-token": "node src/scripts/fetch_MUSIXMATCH_ALT_FALLBACK_TOKEN.mjs",
34
+ "fetch:musixmatch-token": "node src/scripts/fetch_musixmatch_token.mjs",
35
+ "push:musixmatch-token": "node src/scripts/push_musixmatch_token.mjs",
34
36
  "fetch:genius-token": "node src/scripts/fetch_genius_token.mjs",
35
37
  "repro:mcp:arg-boundary": "node src/scripts/mcp-arg-boundary-repro.mjs",
36
38
  "repro:mcp:arg-boundary:sdk": "node src/scripts/mcp-arg-boundary-sdk-repro.mjs"
@@ -25,7 +25,7 @@ async function ensureGeniusAuth() {
25
25
  await getGeniusToken();
26
26
  return;
27
27
  }
28
- assertEnv(['GENIUS_ACCESS_TOKEN']);
28
+ assertEnv(['GENIUS_DIRECT_TOKEN']);
29
29
  }
30
30
 
31
31
  function normalizeHit(hit, query) {
@@ -138,12 +138,11 @@ async function macroRequest(track) {
138
138
  async function ensureMusixmatchToken() {
139
139
  const token = await getMusixmatchToken();
140
140
  if (!token) {
141
- // Neither a fallback token (MUSIXMATCH_FALLBACK_TOKEN / MUSIXMATCH_ALT_USER_TOKEN env vars) nor a
141
+ // Neither a direct token (MUSIXMATCH_DIRECT_TOKEN env var) nor a KV token nor a
142
142
  // cache token (on-disk .cache/musixmatch-token.json) could be found.
143
143
  throw new Error(
144
144
  'Musixmatch token not found. ' +
145
- 'Set MUSIXMATCH_FALLBACK_TOKEN (fallback token recommended for production/ephemeral hosts) ' +
146
- 'or MUSIXMATCH_ALT_USER_TOKEN as an environment variable, ' +
145
+ 'Set MUSIXMATCH_DIRECT_TOKEN as an environment variable (recommended for production/ephemeral hosts), ' +
147
146
  'or run `npm run fetch:musixmatch-token` to populate the on-disk cache token.'
148
147
  );
149
148
  }
@@ -3,7 +3,8 @@ import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
 
5
5
  import axios from 'axios';
6
- import '../src/utils/config.js';
6
+ import '../utils/config.js';
7
+ import { describeKvBackend, isKvConfigured, kvSet } from '../utils/kv-store.js';
7
8
 
8
9
  const TOKEN_ENDPOINT = 'https://api.genius.com/oauth/token';
9
10
 
@@ -17,10 +18,10 @@ function printDeploymentBlock(accessToken) {
17
18
  console.log('LOCAL DEVELOPMENT (cache token)');
18
19
  console.log(' Token written to the cache file above.');
19
20
  console.log(' The server reads it on startup when a writable filesystem is available.\n');
20
- console.log('RENDER / EPHEMERAL DEPLOYMENTS (fallback token)');
21
+ console.log('RENDER / EPHEMERAL DEPLOYMENTS (direct token)');
21
22
  console.log(' If you cannot use client_credentials, set the token as an env var');
22
- console.log(' in your platform dashboard. It acts as a static fallback token:\n');
23
- console.log(` GENIUS_ACCESS_TOKEN=${accessToken}\n`);
23
+ console.log(' in your platform dashboard. It acts as a static direct token:\n');
24
+ console.log(` GENIUS_DIRECT_TOKEN=${accessToken}\n`);
24
25
  console.log(" Note: static tokens don't auto-refresh. Redeploy with a new token");
25
26
  console.log(' if/when it expires. The client_credentials path avoids this entirely.');
26
27
  console.log('─'.repeat(68) + '\n');
@@ -66,6 +67,22 @@ async function main() {
66
67
  console.log(`\nCache token written to: ${cachePath}`);
67
68
  console.log('(The server reads this file on startup when a writable filesystem is available.)');
68
69
 
70
+ // Write to KV store if configured (ephemeral hosts, npx installs).
71
+ if (isKvConfigured()) {
72
+ const kvKey = process.env.GENIUS_TOKEN_KV_KEY || 'mr-magic:genius-token';
73
+ const kvTtl = parseInt(process.env.GENIUS_TOKEN_KV_TTL_SECONDS || '3600', 10);
74
+ const kvPayload = JSON.stringify({
75
+ access_token: accessToken,
76
+ expires_at: Date.now() + (expiresIn || 3600) * 1000
77
+ });
78
+ try {
79
+ await kvSet(kvKey, kvPayload, kvTtl);
80
+ console.log(`Token written to KV store (${describeKvBackend()}) under key: ${kvKey}`);
81
+ } catch (err) {
82
+ console.warn(`Failed to write token to KV store: ${err.message}`);
83
+ }
84
+ }
85
+
69
86
  printDeploymentBlock(accessToken);
70
87
  } catch (error) {
71
88
  console.error('Failed to refresh Genius token:', error.response?.data || error.message);
@@ -2,8 +2,9 @@
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
 
5
- import { chromium } from 'playwright';
6
- import '../src/utils/config.js';
5
+ import { chromium, firefox, webkit } from 'playwright';
6
+ import '../utils/config.js';
7
+ import { describeKvBackend, isKvConfigured, kvSet } from '../utils/kv-store.js';
7
8
 
8
9
  const AUTH_URL = 'https://auth.musixmatch.com/';
9
10
 
@@ -17,8 +18,21 @@ async function saveToken(token, desktopCookie) {
17
18
  payload.desktopCookie = desktopCookie;
18
19
  }
19
20
  await writeFile(cachePath, JSON.stringify(payload, null, 2), 'utf8');
20
- console.log(`\nCache token written to: ${cachePath}`);
21
- console.log('(The server reads this file on startup when a writable filesystem is available.)');
21
+ console.log(`\nToken written to cache: ${cachePath}`);
22
+ console.log('(Local and persistent servers read this file on startup.)');
23
+ }
24
+
25
+ async function saveToKv(token, desktopCookie) {
26
+ if (!isKvConfigured()) return;
27
+ const kvKey = process.env.MUSIXMATCH_TOKEN_KV_KEY || 'mr-magic:musixmatch-token';
28
+ const kvTtl = parseInt(process.env.MUSIXMATCH_TOKEN_KV_TTL_SECONDS || '2592000', 10);
29
+ const payload = JSON.stringify({ token, ...(desktopCookie ? { desktopCookie } : {}) });
30
+ try {
31
+ await kvSet(kvKey, payload, kvTtl);
32
+ console.log(`Token written to KV store (${describeKvBackend()}) under key: ${kvKey}`);
33
+ } catch (error) {
34
+ console.error(`Failed to write token to KV store: ${error.message}`);
35
+ }
22
36
  }
23
37
 
24
38
  function printDeploymentBlock(tokenValue) {
@@ -26,32 +40,158 @@ function printDeploymentBlock(tokenValue) {
26
40
  typeof tokenValue === 'string'
27
41
  ? tokenValue
28
42
  : (tokenValue?.message?.body?.usertoken ?? JSON.stringify(tokenValue));
43
+ const kvBackend = isKvConfigured() ? describeKvBackend() : null;
44
+
29
45
  console.log('\n' + '─'.repeat(68));
30
46
  console.log('Token captured successfully!\n');
31
- console.log('LOCAL DEVELOPMENT (cache token)');
32
- console.log(' The token has been written to the cache file above.');
33
- console.log(' The server loads it at startup — no further action needed.\n');
34
- console.log('RENDER / EPHEMERAL DEPLOYMENTS (fallback token)');
35
- console.log(' The filesystem is wiped on restart, so set the token as an');
36
- console.log(' environment variable in your platform dashboard instead:\n');
37
- console.log(` MUSIXMATCH_FALLBACK_TOKEN=${tokenString}\n`);
38
- console.log(' The server reads MUSIXMATCH_FALLBACK_TOKEN on startup (1st priority)');
39
- console.log(' and never touches the cache file on ephemeral hosts.');
47
+
48
+ console.log('LOCAL & PERSISTENT SERVERS (cache token)');
49
+ console.log(' Token written to .cache/musixmatch-token.json (or MUSIXMATCH_TOKEN_CACHE).');
50
+ console.log(' Any server with a writable, persistent filesystem (local dev, VPS,');
51
+ console.log(' dedicated host) reads it automatically on startup.');
52
+ console.log(' Re-run this script only when your token expires.\n');
53
+
54
+ if (kvBackend) {
55
+ console.log(`EPHEMERAL / NPX INSTALLS KV STORE (${kvBackend})`);
56
+ console.log(` Token written to KV key "mr-magic:musixmatch-token".`);
57
+ console.log(' The server reads it on startup automatically — no extra config needed.');
58
+ console.log(' Re-run this script when your token expires to refresh the KV entry.\n');
59
+ } else {
60
+ console.log('EPHEMERAL / NPX INSTALLS — KV STORE (not configured)');
61
+ console.log(' Set UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN (Upstash Redis)');
62
+ console.log(' or CF_API_TOKEN + CF_ACCOUNT_ID + CF_KV_NAMESPACE_ID (Cloudflare KV)');
63
+ console.log(' and re-run this script to have the token stored in KV automatically.\n');
64
+ }
65
+
66
+ console.log('EPHEMERAL / SERVERLESS — MANUAL ENV VAR OVERRIDE');
67
+ console.log(' Copy the token below and set it in your platform dashboard.');
68
+ console.log(
69
+ ' The server reads MUSIXMATCH_DIRECT_TOKEN on startup (highest priority env var):\n'
70
+ );
71
+ console.log(` MUSIXMATCH_DIRECT_TOKEN=${tokenString}\n`);
72
+
40
73
  console.log('─'.repeat(68) + '\n');
41
74
  }
42
75
 
76
+ function isHeadlessEnabled() {
77
+ const value = (process.env.HEADLESS || '').trim().toLowerCase();
78
+ return value === '1' || value === 'true' || value === 'yes';
79
+ }
80
+
43
81
  async function main() {
44
- console.log('Launching Playwright to acquire Musixmatch token...');
45
- const browser = await chromium.launch({ headless: process.env.HEADLESS !== 'false' });
46
- const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
82
+ const headless = isHeadlessEnabled();
83
+
84
+ // Persistent browser session stores cookies/logins between script runs so you don't
85
+ // have to sign in again until your session actually expires.
86
+ // Override with PLAYWRIGHT_SESSION_DIR env var if you need a different location.
87
+ const sessionDir =
88
+ process.env.PLAYWRIGHT_SESSION_DIR || path.resolve('.cache', 'playwright-session');
89
+ await mkdir(sessionDir, { recursive: true });
90
+
91
+ console.log(`Launching Playwright (headless=${headless}) to acquire Musixmatch token...`);
92
+ console.log(`Browser session directory: ${sessionDir}\n`);
93
+
94
+ // Try real installed browsers in priority order so Google OAuth doesn't block the
95
+ // automated bundled Chromium. Override with BROWSER=<name> to skip straight to one.
96
+ // Chromium channels : chrome, brave, msedge, comet
97
+ // Other engines : firefox, safari (webkit)
98
+ // Last resort : bundled Chromium (may be blocked by Google OAuth)
99
+ //
100
+ // launchPersistentContext() is used instead of launch() + newContext() so the browser
101
+ // session (cookies, logins) is saved to sessionDir and reused on subsequent runs.
102
+ const CHROMIUM_UA =
103
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
104
+ const chromiumArgs = ['--disable-blink-features=AutomationControlled'];
105
+ const baseOpts = { headless, slowMo: headless ? 0 : 150, viewport: { width: 1280, height: 900 } };
106
+ const chromiumOpts = { ...baseOpts, args: chromiumArgs, userAgent: CHROMIUM_UA };
107
+
108
+ // Each launcher returns a BrowserContext (launchPersistentContext skips browser.newContext()).
109
+ const candidates = [
110
+ [
111
+ 'chrome',
112
+ () => chromium.launchPersistentContext(sessionDir, { ...chromiumOpts, channel: 'chrome' })
113
+ ],
114
+ [
115
+ 'brave (channel)',
116
+ () => chromium.launchPersistentContext(sessionDir, { ...chromiumOpts, channel: 'brave' })
117
+ ],
118
+ [
119
+ 'brave (path)',
120
+ () =>
121
+ chromium.launchPersistentContext(sessionDir, {
122
+ ...chromiumOpts,
123
+ executablePath: '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'
124
+ })
125
+ ],
126
+ [
127
+ 'msedge',
128
+ () => chromium.launchPersistentContext(sessionDir, { ...chromiumOpts, channel: 'msedge' })
129
+ ],
130
+ [
131
+ 'comet',
132
+ () =>
133
+ chromium.launchPersistentContext(sessionDir, {
134
+ ...chromiumOpts,
135
+ executablePath: '/Applications/Comet.app/Contents/MacOS/Comet'
136
+ })
137
+ ],
138
+ ['firefox', () => firefox.launchPersistentContext(sessionDir, { ...baseOpts })],
139
+ ['safari (webkit)', () => webkit.launchPersistentContext(sessionDir, { ...baseOpts })],
140
+ ['bundled chromium', () => chromium.launchPersistentContext(sessionDir, { ...chromiumOpts })]
141
+ ];
142
+
143
+ // If BROWSER is set, move that candidate to the front.
144
+ const browserEnv = (process.env.BROWSER || '').trim().toLowerCase();
145
+ const orderedCandidates = browserEnv
146
+ ? [
147
+ ...candidates.filter(([label]) => label.startsWith(browserEnv)),
148
+ ...candidates.filter(([label]) => !label.startsWith(browserEnv))
149
+ ]
150
+ : candidates;
151
+
152
+ let context;
153
+ let chosenLabel;
154
+ for (const [label, launcher] of orderedCandidates) {
155
+ try {
156
+ context = await launcher();
157
+ chosenLabel = label;
158
+ break;
159
+ } catch (err) {
160
+ console.warn(` ${label} not available (${err.message?.split('\n')[0]}), trying next...`);
161
+ }
162
+ }
163
+
164
+ if (!context) {
165
+ console.error('No usable browser found. Install Chrome, Brave, Edge, Firefox, or Safari.');
166
+ process.exit(1);
167
+ }
168
+ console.log(`Using browser: ${chosenLabel}`);
169
+
170
+ // Remove the webdriver flag that Google uses to detect automated browsers.
171
+ await context.addInitScript(() => {
172
+ Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
173
+ });
174
+
47
175
  const page = await context.newPage();
48
176
 
49
- console.log('Navigate to Musixmatch login and sign in.');
50
- await page.goto(AUTH_URL, { waitUntil: 'domcontentloaded' });
51
- console.log('Waiting to be redirected to https://www.musixmatch.com/discover ...');
52
- await page.waitForURL('**/discover', { timeout: 0 });
177
+ page.on('console', (msg) => {
178
+ if (msg.type() === 'error') {
179
+ const text = msg.text();
180
+ // Suppress benign COOP warning emitted by the auth page itself.
181
+ if (text.includes('Cross-Origin-Opener-Policy')) return;
182
+ console.error(`[browser console error] ${text}`);
183
+ }
184
+ });
185
+ page.on('pageerror', (err) => console.error(`[browser page error] ${err.message}`));
186
+
187
+ console.log(`Navigating to ${AUTH_URL} — sign in in the browser window that appears.`);
188
+ // 'commit' fires as soon as the server response starts (before content loads), which avoids
189
+ // ERR_ABORTED on browsers like Comet that intercept or redirect during initial navigation.
190
+ await page.goto(AUTH_URL, { waitUntil: 'commit' });
191
+ console.log('Waiting to be redirected to https://account.musixmatch.com/ ...');
192
+ await page.waitForURL('https://account.musixmatch.com/**', { timeout: 0 });
53
193
 
54
- const cookies = await context.cookies('https://www.musixmatch.com');
194
+ const cookies = await context.cookies('https://account.musixmatch.com');
55
195
  const userCookie = cookies.find((cookie) => cookie.name === 'musixmatchUserToken');
56
196
  const desktopCookie = cookies.find((cookie) => cookie.name === 'web-desktop-app-v1.0');
57
197
  if (!userCookie) {
@@ -70,14 +210,20 @@ async function main() {
70
210
  console.log('\nMusixmatch token payload:');
71
211
  console.log(JSON.stringify(parsed, null, 2));
72
212
 
73
- await saveToken(parsed, desktopCookie ? decodeURIComponent(desktopCookie.value) : null);
213
+ const decodedDesktopCookie = desktopCookie ? decodeURIComponent(desktopCookie.value) : null;
214
+
215
+ // Write to all configured storage backends in parallel.
216
+ await Promise.allSettled([
217
+ saveToken(parsed, decodedDesktopCookie),
218
+ saveToKv(parsed, decodedDesktopCookie)
219
+ ]);
74
220
 
75
221
  // Extract the raw token string for the deployment hint.
76
222
  // The parsed payload is the full musixmatchUserToken JSON object; the server
77
223
  // stores and reads the entire parsed object as the `token` field.
78
224
  printDeploymentBlock(parsed);
79
225
 
80
- await browser.close();
226
+ await context.close();
81
227
  }
82
228
 
83
229
  main().catch((error) => {
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * push_musixmatch_token.mjs
4
+ *
5
+ * Seed the Musixmatch token to all configured storage backends (Upstash Redis,
6
+ * Cloudflare KV, and/or on-disk cache) WITHOUT opening a browser.
7
+ *
8
+ * Use this when you already have a token value — e.g. captured once locally via
9
+ * `npm run fetch:musixmatch-token` — and need to push it to a headless server,
10
+ * ephemeral deployment (Render), or CI/CD pipeline where a browser is unavailable.
11
+ *
12
+ * Usage (env var — recommended for Render / build/start commands):
13
+ * MUSIXMATCH_DIRECT_TOKEN='{"message":...}' npm run push:musixmatch-token
14
+ *
15
+ * Usage (CLI flag):
16
+ * npm run push:musixmatch-token -- --token '{"message":...}'
17
+ *
18
+ * The token value must be the full musixmatchUserToken JSON payload (the same
19
+ * object that `fetch:musixmatch-token` captures and prints after sign-in).
20
+ * A raw string token is also accepted.
21
+ *
22
+ * Exit codes:
23
+ * 0 — token pushed successfully (or no token provided, no-op)
24
+ * 1 — token was provided but a push failure occurred (KV write error, etc.)
25
+ *
26
+ * Render example (build command or start command):
27
+ * MUSIXMATCH_DIRECT_TOKEN='...' npm run push:musixmatch-token && npm run server:mcp:http
28
+ */
29
+
30
+ import { mkdir, writeFile } from 'node:fs/promises';
31
+ import path from 'node:path';
32
+ import { parseArgs } from 'node:util';
33
+
34
+ import '../utils/config.js';
35
+ import { describeKvBackend, isKvConfigured, kvSet } from '../utils/kv-store.js';
36
+
37
+ // ─── Argument parsing ─────────────────────────────────────────────────────────
38
+
39
+ const { values } = parseArgs({
40
+ args: process.argv.slice(2),
41
+ options: {
42
+ token: { type: 'string', short: 't' },
43
+ help: { type: 'boolean', short: 'h' }
44
+ },
45
+ strict: false
46
+ });
47
+
48
+ if (values.help) {
49
+ console.log(`
50
+ push_musixmatch_token — seed Musixmatch token to all configured backends
51
+
52
+ Usage:
53
+ MUSIXMATCH_DIRECT_TOKEN='<json_or_string>' npm run push:musixmatch-token
54
+ npm run push:musixmatch-token -- --token '<json_or_string>'
55
+
56
+ The token value is the full musixmatchUserToken JSON payload captured by
57
+ fetch:musixmatch-token, or a raw string token.
58
+
59
+ Backends written (if configured):
60
+ • Upstash Redis — UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN
61
+ • Cloudflare KV — CF_API_TOKEN + CF_ACCOUNT_ID + CF_KV_NAMESPACE_ID
62
+ • On-disk cache — .cache/musixmatch-token.json (or MUSIXMATCH_TOKEN_CACHE)
63
+
64
+ If MUSIXMATCH_DIRECT_TOKEN is not set and --token is not supplied, the
65
+ script exits 0 with no output (safe to chain in build/start commands).
66
+ `);
67
+ process.exit(0);
68
+ }
69
+
70
+ // ─── Main ─────────────────────────────────────────────────────────────────────
71
+
72
+ async function main() {
73
+ const rawToken = values.token || process.env.MUSIXMATCH_DIRECT_TOKEN;
74
+
75
+ // If no token is provided, exit silently so this can be safely chained in
76
+ // build/start commands when the token hasn't been set yet.
77
+ if (!rawToken) {
78
+ return;
79
+ }
80
+
81
+ // Parse as JSON if possible; otherwise treat as a raw string token.
82
+ let parsedToken;
83
+ try {
84
+ parsedToken = JSON.parse(rawToken);
85
+ } catch {
86
+ parsedToken = rawToken;
87
+ }
88
+
89
+ console.log('Pushing Musixmatch token to configured backends...');
90
+ let anyFailed = false;
91
+
92
+ // ─── On-disk cache ───────────────────────────────────────────────────────
93
+ const cachePath =
94
+ process.env.MUSIXMATCH_TOKEN_CACHE || path.resolve('.cache', 'musixmatch-token.json');
95
+ try {
96
+ await mkdir(path.dirname(cachePath), { recursive: true });
97
+ await writeFile(cachePath, JSON.stringify({ token: parsedToken }, null, 2), 'utf8');
98
+ console.log(` ✓ Disk cache: ${cachePath}`);
99
+ } catch (err) {
100
+ console.warn(` ✗ Disk cache write failed (${err.message}) — continuing.`);
101
+ // Not fatal; remote hosts may not have a writable FS.
102
+ }
103
+
104
+ // ─── KV store ─────────────────────────────────────────────────────────────
105
+ if (isKvConfigured()) {
106
+ const kvKey = process.env.MUSIXMATCH_TOKEN_KV_KEY || 'mr-magic:musixmatch-token';
107
+ const kvTtl = parseInt(process.env.MUSIXMATCH_TOKEN_KV_TTL_SECONDS || '2592000', 10);
108
+ const payload = JSON.stringify({ token: parsedToken });
109
+ try {
110
+ await kvSet(kvKey, payload, kvTtl);
111
+ console.log(` ✓ KV store (${describeKvBackend()}): key="${kvKey}", ttl=${kvTtl}s`);
112
+ } catch (err) {
113
+ console.error(` ✗ KV store write failed: ${err.message}`);
114
+ anyFailed = true;
115
+ }
116
+ } else {
117
+ console.log(
118
+ ' — KV store: not configured (set UPSTASH_REDIS_REST_URL/TOKEN or CF_* vars to enable)'
119
+ );
120
+ }
121
+
122
+ if (anyFailed) {
123
+ console.error('\n✗ One or more backends failed — see errors above.');
124
+ process.exit(1);
125
+ }
126
+
127
+ console.log('\n✓ Token pushed. The server will read it from the available backend on startup.');
128
+ }
129
+
130
+ main().catch((err) => {
131
+ console.error('Unexpected error:', err.message);
132
+ process.exit(1);
133
+ });
@@ -435,11 +435,10 @@ export async function handleMcpTool(name, args = {}) {
435
435
 
436
436
  if (name === 'runtime_status') {
437
437
  const CREDENTIAL_KEYS = [
438
- 'GENIUS_ACCESS_TOKEN',
438
+ 'GENIUS_DIRECT_TOKEN',
439
439
  'GENIUS_CLIENT_ID',
440
440
  'GENIUS_CLIENT_SECRET',
441
- 'MUSIXMATCH_ALT_USER_TOKEN',
442
- 'MUSIXMATCH_FALLBACK_TOKEN',
441
+ 'MUSIXMATCH_DIRECT_TOKEN',
443
442
  'MELON_COOKIE'
444
443
  ];
445
444
  return {
@@ -37,7 +37,7 @@ async function logGeniusStatus(context) {
37
37
  context,
38
38
  provider: 'genius',
39
39
  clientCredentialsPresent: diagnostics.clientCredentialsPresent,
40
- fallbackTokenPresent: diagnostics.fallbackTokenPresent,
40
+ directTokenPresent: diagnostics.directTokenPresent,
41
41
  cacheTokenPresent: diagnostics.cacheTokenPresent,
42
42
  cacheTokenExpired: diagnostics.cacheTokenExpired
43
43
  });
@@ -73,7 +73,7 @@ async function logGeniusStatus(context) {
73
73
  logger.warn('Genius credentials missing', {
74
74
  context,
75
75
  provider: 'genius',
76
- details: 'set GENIUS_CLIENT_ID/SECRET or GENIUS_ACCESS_TOKEN'
76
+ details: 'set GENIUS_CLIENT_ID/SECRET or GENIUS_DIRECT_TOKEN'
77
77
  });
78
78
  }
79
79
  }
@@ -86,8 +86,7 @@ async function logMusixmatchStatus(context) {
86
86
  context,
87
87
  provider: 'musixmatch',
88
88
  cachePath: diagnostics.cachePath,
89
- userEnvPresent: diagnostics.userEnvPresent,
90
- envPresent: diagnostics.envPresent,
89
+ directEnvPresent: diagnostics.directEnvPresent,
91
90
  runtimeTokenCached: diagnostics.runtimeTokenCached,
92
91
  lastLoadedFrom: diagnostics.lastLoadedFrom
93
92
  });
@@ -117,7 +116,7 @@ async function logMusixmatchStatus(context) {
117
116
  context,
118
117
  provider: 'musixmatch',
119
118
  details:
120
- 'run npm run fetch:musixmatch-token to capture the cache token, or set MUSIXMATCH_FALLBACK_TOKEN as a fallback token for ephemeral deployments'
119
+ 'run npm run fetch:musixmatch-token to capture the cache token, or set MUSIXMATCH_DIRECT_TOKEN as a direct token for ephemeral deployments'
121
120
  });
122
121
  }
123
122
  }
@@ -23,7 +23,7 @@ export function getEnvValue(name) {
23
23
  return process.env[name] ?? null;
24
24
  }
25
25
 
26
- const DEFAULT_REQUIRED = ['GENIUS_ACCESS_TOKEN'];
26
+ const DEFAULT_REQUIRED = ['GENIUS_DIRECT_TOKEN'];
27
27
  const warnedMissingEnvCache = new Set();
28
28
 
29
29
  function getMissingEnvVars(requiredVars = DEFAULT_REQUIRED) {