mr-magic-mcp-server 0.1.6 → 0.1.8

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 CHANGED
@@ -3,13 +3,27 @@
3
3
  # At minimum one of these is needed for Genius lyrics support.
4
4
  # Get all three from https://genius.com/api-clients
5
5
  # ───────────────────────────────────────────────────────────────────────────────
6
- # Simplest: supply GENIUS_ACCESS_TOKEN alone (static token, no auto-refresh).
7
- # Better: supply CLIENT_ID + CLIENT_SECRET — the server will auto-refresh via
8
- # OAuth client_credentials, making ACCESS_TOKEN optional.
6
+ # There are three ways to supply the Genius token (tried in order):
7
+ #
8
+ # Auto-refresh (recommended for all deployments, including Render/ephemeral)
9
+ # Set GENIUS_CLIENT_ID + GENIUS_CLIENT_SECRET. The server calls the Genius
10
+ # OAuth client_credentials endpoint at runtime and refreshes the token in
11
+ # memory automatically. No disk, no scripts, no manual token copying needed.
12
+ #
13
+ # Fallback token (env var) — static bearer token, no auto-refresh
14
+ # Set GENIUS_ACCESS_TOKEN. Works everywhere but the token must be
15
+ # manually updated on expiry (or redeploy with a new value).
16
+ # Use this only when client_credentials are unavailable.
17
+ #
18
+ # Cache token (on-disk) — local development only
19
+ # Run `npm run fetch:genius-token` to write a token to
20
+ # `.cache/genius-token.json`. The server reads it on startup when a
21
+ # persistent, writable filesystem is available. Not suitable for
22
+ # ephemeral hosts.
9
23
  # ═══════════════════════════════════════════════════════════════════════════════
10
- GENIUS_ACCESS_TOKEN= # Static bearer token (required if not using client credentials)
11
- GENIUS_CLIENT_ID= # OAuth client ID (enables auto-refresh)
12
- GENIUS_CLIENT_SECRET= # OAuth client secret (enables auto-refresh)
24
+ GENIUS_CLIENT_ID= # Auto-refresh: OAuth client ID (enables runtime auto-refresh)
25
+ GENIUS_CLIENT_SECRET= # Auto-refresh: OAuth client secret (enables runtime auto-refresh)
26
+ GENIUS_ACCESS_TOKEN= # Fallback token: static bearer token (required if not using client credentials)
13
27
 
14
28
  # ═══════════════════════════════════════════════════════════════════════════════
15
29
  # REQUIRED (feature-specific) — Musixmatch
@@ -18,20 +32,19 @@ GENIUS_CLIENT_SECRET= # OAuth client secret (enables auto-refresh)
18
32
  # ───────────────────────────────────────────────────────────────────────────────
19
33
  # There are two ways to supply the Musixmatch token:
20
34
  #
21
- # Fallback token (env var) — recommended for production / ephemeral hosts
22
- # Set MUSIXMATCH_USER_TOKEN or MUSIXMATCH_ALT_USER_TOKEN as an environment variable.
23
- # Use this when persistent filesystem storage is not available (e.g. Render
24
- # free tier, serverless, containers without a mounted volume).
25
- # Resolution order: MUSIXMATCH_USER_TOKEN (1st) → MUSIXMATCH_ALT_USER_TOKEN (2nd)
35
+ # Fallback token (env var) — recommended for production / ephemeral hosts
36
+ # Set MUSIXMATCH_FALLBACK_TOKEN or MUSIXMATCH_ALT_FALLBACK_TOKEN as an environment variable.
37
+ # Use this when persistent filesystem storage is not available (e.g. Render
38
+ # free tier, serverless, containers without a mounted volume).
39
+ # Resolution order: MUSIXMATCH_FALLBACK_TOKEN (1st) → MUSIXMATCH_ALT_FALLBACK_TOKEN (2nd)
26
40
  #
27
- # Cache token (on-disk) — local development only
28
- # Run `npm run fetch:musixmatch-token` to sign in via Playwright and write
29
- # the token to `.cache/musixmatch-token.json`. The server reads it on startup.
30
- # Not suitable for ephemeral hosts where the filesystem is wiped on restart.
41
+ # Cache token (on-disk) — local development only
42
+ # Run `npm run fetch:musixmatch-token` to sign in via Playwright and write
43
+ # the token to `.cache/musixmatch-token.json`. The server reads it on startup.
44
+ # Not suitable for ephemeral hosts where the filesystem is wiped on restart.
31
45
  # ═══════════════════════════════════════════════════════════════════════════════
32
- MUSIXMATCH_USER_TOKEN= # Fallback token (1st priority) — set this in production
33
- MUSIXMATCH_ALT_USER_TOKEN= # Fallback token (2nd priority) — alternative env var
34
-
46
+ MUSIXMATCH_FALLBACK_TOKEN= # Fallback token (1st priority) — set this in production
47
+ MUSIXMATCH_ALT_FALLBACK_TOKEN= # Fallback token (2nd priority) — alternative env var
35
48
 
36
49
  # ═══════════════════════════════════════════════════════════════════════════════
37
50
  # REQUIRED (feature-specific) — Airtable
@@ -91,8 +104,9 @@ MR_MAGIC_INLINE_PAYLOAD_MAX_CHARS=1500
91
104
  # Export TTL in seconds for local and redis backends (default: 3600 / 1 hour)
92
105
  MR_MAGIC_EXPORT_TTL_SECONDS=3600
93
106
 
94
- # Musixmatch: override the on-disk token cache path (local dev only)
95
- MUSIXMATCH_ALT_USER_TOKEN_CACHE=.cache/musixmatch-token.json
107
+ # Override the on-disk cache token paths (local dev only)
108
+ GENIUS_TOKEN_CACHE=.cache/genius-token.json
109
+ MUSIXMATCH_TOKEN_CACHE=.cache/musixmatch-token.json
96
110
 
97
111
  # Musixmatch: when 1, provider will attempt to re-run the fetch script
98
112
  # automatically (headless) if no token is available
package/README.md CHANGED
@@ -84,10 +84,10 @@ MR_MAGIC_ENV_PATH= # Optional. Custom path to an env file when the default isn't
84
84
  GENIUS_CLIENT_ID= # Get from https://genius.com/api-clients, required for Genius client-credentials auth.
85
85
  GENIUS_CLIENT_SECRET= # Get from https://genius.com/api-clients, required for Genius client-credentials auth.
86
86
  GENIUS_ACCESS_TOKEN= # Get from https://genius.com/api-clients, required for Genius lyrics support when client credentials are not supplied.
87
- MUSIXMATCH_USER_TOKEN= # Fallback token (1st priority). Set as env var for production/ephemeral hosts where the filesystem is not persistent.
88
- MUSIXMATCH_ALT_USER_TOKEN= # Fallback token (2nd priority). Alternative env var; same token value, second-choice source.
87
+ MUSIXMATCH_FALLBACK_TOKEN= # Fallback token (1st priority). Set as env var for production/ephemeral hosts where the filesystem is not persistent.
88
+ MUSIXMATCH_ALT_FALLBACK_TOKEN= # Fallback token (2nd priority). Alternative env var; same token value, second-choice source.
89
89
  MUSIXMATCH_AUTO_FETCH=0 # Optional. When 1, provider will attempt to re-run the fetch script automatically (headless) if no token is available.
90
- MUSIXMATCH_ALT_USER_TOKEN_CACHE=.cache/musixmatch-token.json
90
+ MUSIXMATCH_TOKEN_CACHE=.cache/musixmatch-token.json
91
91
  MELON_COOKIE= # Optional. Pin a session cookie for consistent Melon results; anonymous access generally works without it.
92
92
  MR_MAGIC_EXPORT_BACKEND= # local|inline|redis
93
93
  MR_MAGIC_EXPORT_DIR=/absolute/path/to/exports # Required if MR_MAGIC_EXPORT_BACKEND=local
@@ -105,25 +105,31 @@ UPSTASH_REDIS_REST_TOKEN= # Get from https://console.upstash.com/redis/rest, req
105
105
  AIRTABLE_PERSONAL_ACCESS_TOKEN= # Required for push_catalog_to_airtable tool. Get from https://airtable.com/create/tokens
106
106
  ```
107
107
 
108
- - **GENIUS_ACCESS_TOKEN** is required for Genius lyrics support. The
109
- CLI/servers will reject Genius requests if it is unset (unless
110
- `GENIUS_CLIENT_ID`/`GENIUS_CLIENT_SECRET` are set for auto-refresh).
111
- - **GENIUS_CLIENT_ID**/**GENIUS_CLIENT_SECRET** can be supplied as an
112
- alternative Genius auth path when you want runtime token refresh instead of a
113
- static access token.
108
+ - **Genius token sources** the server resolves the Genius token using three
109
+ sources (tried in order):
110
+ - **Auto-refresh** (`GENIUS_CLIENT_ID` + `GENIUS_CLIENT_SECRET`) the server calls the
111
+ Genius OAuth `client_credentials` endpoint at runtime and keeps the token refreshed
112
+ in memory automatically. **Recommended for all deployments**, including Render/ephemeral
113
+ hosts. No disk, no scripts, no manual token copying.
114
+ - **Fallback token** (`GENIUS_ACCESS_TOKEN` env var) — a static bearer token. Works
115
+ everywhere but does not auto-refresh. Use only when client_credentials are unavailable;
116
+ redeploy with a new token when it expires.
117
+ - **Cache token** (on-disk `.cache/genius-token.json`) — written by
118
+ `npm run fetch:genius-token`. Loaded on startup when a persistent writable filesystem
119
+ is available. Not suitable for ephemeral hosts.
114
120
  - **Musixmatch token sources** — the server resolves the Musixmatch token using
115
121
  two named sources, tried in order:
116
- - **Fallback token** (`MUSIXMATCH_USER_TOKEN`, then `MUSIXMATCH_ALT_USER_TOKEN`) — the
122
+ - **Fallback token** (`MUSIXMATCH_FALLBACK_TOKEN`, then `MUSIXMATCH_ALT_FALLBACK_TOKEN`) — the
117
123
  token value is set directly as an environment variable. This is the
118
124
  recommended approach for production and ephemeral hosts (e.g. Render free
119
125
  tier, containers) where the filesystem cannot be relied upon between
120
- restarts. Set `MUSIXMATCH_USER_TOKEN` first; `MUSIXMATCH_ALT_USER_TOKEN` is the
126
+ restarts. Set `MUSIXMATCH_FALLBACK_TOKEN` first; `MUSIXMATCH_ALT_FALLBACK_TOKEN` is the
121
127
  legacy/alternative env var for the same value.
122
128
  - **Cache token** (on-disk `.cache/musixmatch-token.json`) — written by the
123
129
  `fetch:musixmatch-token` script after a browser sign-in. Used for local
124
130
  development when a persistent writable filesystem is available. Not suitable
125
131
  for ephemeral hosts.
126
- - **MUSIXMATCH_ALT_USER_TOKEN_CACHE** controls where the on-disk cache token file is
132
+ - **MUSIXMATCH_TOKEN_CACHE** controls where the on-disk cache token file is
127
133
  read/written (default `<project root>/.cache/musixmatch-token.json`).
128
134
  - **MELON_COOKIE** is optional—anonymous access generally works, but pinning a
129
135
  cookie can improve consistency.
@@ -188,7 +194,7 @@ in one of two ways depending on your deployment:
188
194
  `.cache/musixmatch-token.json`. The server loads it on startup whenever a
189
195
  persistent, writable filesystem is available.
190
196
  - **Fallback token** (production/ephemeral): copy the captured token value and
191
- set it as `MUSIXMATCH_USER_TOKEN` (recommended) or `MUSIXMATCH_ALT_USER_TOKEN` in your
197
+ set it as `MUSIXMATCH_FALLBACK_TOKEN` (recommended) or `MUSIXMATCH_ALT_FALLBACK_TOKEN` in your
192
198
  platform's environment. This is the only reliable option on ephemeral hosts
193
199
  (Render free tier, containers without a mounted volume) where the filesystem
194
200
  is wiped between restarts.
@@ -214,7 +220,7 @@ in one of two ways depending on your deployment:
214
220
  the session.
215
221
 
216
222
  4. **For remote/ephemeral deployments:** copy the `token` value from the
217
- printed JSON and set it as `MUSIXMATCH_USER_TOKEN` in your platform
223
+ printed JSON and set it as `MUSIXMATCH_FALLBACK_TOKEN` in your platform
218
224
  environment (the fallback token). Do **not** rely on the cache file
219
225
  surviving a restart on ephemeral hosts.
220
226
  If `MUSIXMATCH_AUTO_FETCH=1`, the provider can attempt to re-run the fetch
@@ -224,7 +230,7 @@ in one of two ways depending on your deployment:
224
230
  #### Developer Accounts
225
231
 
226
232
  1. Get API access from `https://developer.musixmatch.com`
227
- 2. Run the script above and set the resulting token as `MUSIXMATCH_USER_TOKEN`
233
+ 2. Run the script above and set the resulting token as `MUSIXMATCH_FALLBACK_TOKEN`
228
234
  (fallback token) in your environment, or keep the on-disk cache token in sync
229
235
  for local development.
230
236
 
@@ -233,7 +239,7 @@ in one of two ways depending on your deployment:
233
239
  1. Visit `https://auth.musixmatch.com/`
234
240
  2. Sign in with a Musixmatch account and allow the app. When redirected, the
235
241
  helper script above will capture the session and write the cache token.
236
- 3. Copy the `token` value and set it as `MUSIXMATCH_USER_TOKEN` for any remote
242
+ 3. Copy the `token` value and set it as `MUSIXMATCH_FALLBACK_TOKEN` for any remote
237
243
  environment that needs it.
238
244
 
239
245
  **WARNING: CALLING THE API FROM AN UNAUTHORIZED ACCOUNT MAY RESULT IN A BAN.**
@@ -594,7 +600,7 @@ Add env vars inline if your client supports the `env` field:
594
600
  "args": ["-y", "mr-magic-mcp-server"],
595
601
  "env": {
596
602
  "GENIUS_ACCESS_TOKEN": "...",
597
- "MUSIXMATCH_USER_TOKEN": "...",
603
+ "MUSIXMATCH_FALLBACK_TOKEN": "...",
598
604
  "AIRTABLE_PERSONAL_ACCESS_TOKEN": "..."
599
605
  }
600
606
  }
@@ -1118,13 +1124,12 @@ For direct binary usage, use `mrmagic-cli search --artist ... --title ...`.
1118
1124
  ## Provider notes
1119
1125
 
1120
1126
  - **LRCLIB**: Public API with synced lyric coverage; no auth required.
1121
- - **Genius**: Requires `GENIUS_ACCESS_TOKEN`. Provides metadata-rich plain
1122
- lyrics.
1127
+ - **Genius**: Requires credentials — either `GENIUS_CLIENT_ID` + `GENIUS_CLIENT_SECRET`
1128
+ for auto-refresh (recommended) or `GENIUS_ACCESS_TOKEN` as a static fallback token.
1123
1129
  - **Musixmatch**: Requires a token — either a **fallback token** set via
1124
- `MUSIXMATCH_USER_TOKEN` (recommended for production) or `MUSIXMATCH_ALT_USER_TOKEN`
1125
- env vars, or a **cache token** written to disk by
1126
- `scripts/fetch_MUSIXMATCH_ALT_USER_TOKEN.mjs` (local dev). See "Getting the Musixmatch
1127
- token" above for the full workflow.
1130
+ `MUSIXMATCH_FALLBACK_TOKEN` (recommended for production) or `MUSIXMATCH_ALT_FALLBACK_TOKEN` env var,
1131
+ or a **cache token** written to disk by `npm run fetch:musixmatch-token` (local dev).
1132
+ See "Getting the Musixmatch token" above for the full workflow.
1128
1133
  - **Melon**: Works anonymously but benefits from `MELON_COOKIE` for reliability
1129
1134
  if needed.
1130
1135
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-magic-mcp-server",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Lyrics MCP server connecting LRCLIB, Genius, Musixmatch, and Melon",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -30,7 +30,7 @@
30
30
  "format": "prettier --write .",
31
31
  "format:check": "prettier --check .",
32
32
  "test": "node tests/run-tests.js",
33
- "fetch:musixmatch-token": "node scripts/fetch_MUSIXMATCH_ALT_USER_TOKEN.mjs",
33
+ "fetch:musixmatch-token": "node scripts/fetch_MUSIXMATCH_ALT_FALLBACK_TOKEN.mjs",
34
34
  "fetch:genius-token": "node scripts/fetch_genius_token.mjs",
35
35
  "repro:mcp:arg-boundary": "node scripts/mcp-arg-boundary-repro.mjs",
36
36
  "repro:mcp:arg-boundary:sdk": "node scripts/mcp-arg-boundary-sdk-repro.mjs",
@@ -138,11 +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_USER_TOKEN / MUSIXMATCH_ALT_USER_TOKEN env vars) nor a
141
+ // Neither a fallback token (MUSIXMATCH_FALLBACK_TOKEN / MUSIXMATCH_ALT_USER_TOKEN env vars) 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_USER_TOKEN (fallback token — recommended for production/ephemeral hosts) ' +
145
+ 'Set MUSIXMATCH_FALLBACK_TOKEN (fallback token — recommended for production/ephemeral hosts) ' +
146
146
  'or MUSIXMATCH_ALT_USER_TOKEN as an environment variable, ' +
147
147
  'or run `npm run fetch:musixmatch-token` to populate the on-disk cache token.'
148
148
  );
@@ -13,6 +13,7 @@ import { mcpToolDefinitions, handleMcpTool } from './mcp-tools.js';
13
13
  import { buildMcpResponse } from './mcp-response.js';
14
14
  import { logTokenStatus } from './token-startup-log.js';
15
15
  import { normalizeToolArgs } from './tool-args.js';
16
+ import { getProviderStatus } from '../index.js';
16
17
 
17
18
  function getBodyShape(body) {
18
19
  if (body == null) return 'nullish';
@@ -91,6 +92,10 @@ export async function startMcpHttpServer(options = {}) {
91
92
  await logTokenStatus({ context: 'http-mcp' });
92
93
 
93
94
  const app = createMcpExpressApp({ host });
95
+ app.get('/health', async (_req, res) => {
96
+ res.json({ status: 'ok', providers: await getProviderStatus() });
97
+ });
98
+
94
99
  app.all('/mcp', async (req, res) => {
95
100
  const normalizedBody = normalizeIncomingRpcBody(req.body);
96
101
  const requestId = randomUUID();
@@ -439,7 +439,7 @@ export async function handleMcpTool(name, args = {}) {
439
439
  'GENIUS_CLIENT_ID',
440
440
  'GENIUS_CLIENT_SECRET',
441
441
  'MUSIXMATCH_ALT_USER_TOKEN',
442
- 'MUSIXMATCH_USER_TOKEN',
442
+ 'MUSIXMATCH_FALLBACK_TOKEN',
443
443
  'MELON_COOKIE'
444
444
  ];
445
445
  return {
@@ -29,7 +29,7 @@ export async function logTokenStatus({ context }) {
29
29
  }
30
30
 
31
31
  async function logGeniusStatus(context) {
32
- const diagnostics = getGeniusDiagnostics();
32
+ const diagnostics = await getGeniusDiagnostics();
33
33
  const ready = hasValidGeniusAuth();
34
34
  const mode = describeGeniusAuthMode();
35
35
 
@@ -37,7 +37,9 @@ async function logGeniusStatus(context) {
37
37
  context,
38
38
  provider: 'genius',
39
39
  clientCredentialsPresent: diagnostics.clientCredentialsPresent,
40
- fallbackTokenPresent: diagnostics.fallbackTokenPresent
40
+ fallbackTokenPresent: diagnostics.fallbackTokenPresent,
41
+ cacheTokenPresent: diagnostics.cacheTokenPresent,
42
+ cacheTokenExpired: diagnostics.cacheTokenExpired
41
43
  });
42
44
 
43
45
  if (diagnostics.runtimeTokenCached) {
@@ -114,7 +116,8 @@ async function logMusixmatchStatus(context) {
114
116
  logger.warn('Musixmatch token missing', {
115
117
  context,
116
118
  provider: 'musixmatch',
117
- details: 'run scripts/fetch_MUSIXMATCH_ALT_USER_TOKEN.mjs to capture cookies'
119
+ 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'
118
121
  });
119
122
  }
120
123
  }
@@ -1,11 +1,32 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
1
4
  import axios from 'axios';
2
5
 
3
- import { getEnvValue } from '../config.js';
6
+ import { getEnvValue, getProjectRoot } from '../config.js';
4
7
  import { createLogger } from '../logger.js';
5
8
 
6
9
  const GENIUS_TOKEN_ENDPOINT = 'https://api.genius.com/oauth/token';
7
10
  const logger = createLogger('genius-token-manager');
8
11
 
12
+ // Token source terminology used throughout this module:
13
+ // • Auto-refresh — GENIUS_CLIENT_ID + GENIUS_CLIENT_SECRET env vars.
14
+ // The server calls the Genius OAuth client_credentials endpoint
15
+ // at runtime and keeps the token refreshed in memory automatically.
16
+ // This is the recommended approach for all deployments: no disk,
17
+ // no scripts, and no manual token copying needed.
18
+ // • Fallback token — GENIUS_ACCESS_TOKEN env var. A static bearer token set directly
19
+ // in the environment. Does not auto-refresh; redeploy when expired.
20
+ // Use this only when client_credentials are unavailable.
21
+ // • Cache token — on-disk .cache/genius-token.json written by the fetch script.
22
+ // Only reliable when a persistent, writable filesystem is available
23
+ // (i.e. local development). Ephemeral hosts (Render free tier, etc.)
24
+ // should use the auto-refresh or fallback token paths instead.
25
+
26
+ // Token cache path — must match the path used by scripts/fetch_genius_token.mjs.
27
+ const TOKEN_CACHE_PATH =
28
+ process.env.GENIUS_TOKEN_CACHE || path.join(getProjectRoot(), '.cache', 'genius-token.json');
29
+
9
30
  let cachedToken = null;
10
31
  let cachedExpiry = 0;
11
32
  let lastAuthMode = 'unknown';
@@ -20,6 +41,21 @@ function tokenExpired() {
20
41
  return now >= cachedExpiry - 60_000; // refresh one minute early
21
42
  }
22
43
 
44
+ async function readCachedToken() {
45
+ try {
46
+ const raw = await fs.readFile(TOKEN_CACHE_PATH, 'utf8');
47
+ const parsed = JSON.parse(raw);
48
+ const { access_token: accessToken, expires_at: expiresAt } = parsed ?? {};
49
+ if (!accessToken) return null;
50
+ // Skip if the cache token has already expired (with a 1-minute buffer).
51
+ if (expiresAt && Date.now() >= expiresAt - 60_000) return null;
52
+ return { accessToken, expiresAt };
53
+ } catch {
54
+ // Cache file absent or unreadable — not an error in remote environments.
55
+ return null;
56
+ }
57
+ }
58
+
23
59
  async function fetchClientCredentialsToken() {
24
60
  const clientId = getEnvValue('GENIUS_CLIENT_ID');
25
61
  const clientSecret = getEnvValue('GENIUS_CLIENT_SECRET');
@@ -55,22 +91,44 @@ async function fetchClientCredentialsToken() {
55
91
  }
56
92
  }
57
93
 
94
+ /**
95
+ * Resolve the Genius token using the following priority order:
96
+ * 1. In-memory runtime cache (already resolved this session)
97
+ * 2. Auto-refresh via GENIUS_CLIENT_ID + GENIUS_CLIENT_SECRET (client_credentials)
98
+ * 3. GENIUS_ACCESS_TOKEN env var — fallback token
99
+ * 4. On-disk .cache/genius-token.json — cache token, local dev only
100
+ */
58
101
  export async function getGeniusToken({ forceRefresh = false } = {}) {
59
102
  if (!forceRefresh && !tokenExpired()) {
60
103
  return cachedToken;
61
104
  }
105
+
106
+ // 2. Auto-refresh via client_credentials (ideal for all deployments)
62
107
  const token = await fetchClientCredentialsToken();
63
108
  if (token) {
64
109
  return token;
65
110
  }
111
+
112
+ // 3. Fallback token from env var (static, no auto-refresh)
66
113
  const fallback = getFallbackToken();
67
114
  if (fallback && fallback !== cachedToken) {
68
- logger.warn('Using fallback Genius access token from environment');
115
+ logger.warn('Using Genius fallback token from GENIUS_ACCESS_TOKEN env var');
69
116
  cachedToken = fallback;
70
117
  cachedExpiry = Date.now() + 86_400_000; // 1 day placeholder
71
118
  lastAuthMode = 'env_access_token';
72
119
  return cachedToken;
73
120
  }
121
+
122
+ // 4. Cache token from disk (local dev convenience only)
123
+ const cached = await readCachedToken();
124
+ if (cached) {
125
+ logger.info('Using Genius cache token from disk', { cachePath: TOKEN_CACHE_PATH });
126
+ cachedToken = cached.accessToken;
127
+ cachedExpiry = cached.expiresAt ?? Date.now() + 86_400_000;
128
+ lastAuthMode = 'cache';
129
+ return cachedToken;
130
+ }
131
+
74
132
  return cachedToken;
75
133
  }
76
134
 
@@ -103,17 +161,34 @@ export function describeGeniusAuthMode() {
103
161
  return 'none';
104
162
  }
105
163
 
106
- export function getGeniusDiagnostics() {
164
+ export async function getGeniusDiagnostics() {
107
165
  const clientId = getEnvValue('GENIUS_CLIENT_ID');
108
166
  const clientSecret = getEnvValue('GENIUS_CLIENT_SECRET');
109
167
  const fallback = getFallbackToken();
110
168
  const ttlMs = Math.max(cachedExpiry - Date.now(), 0);
111
169
 
112
- return {
170
+ const diagnostics = {
113
171
  clientCredentialsPresent: Boolean(clientId && clientSecret),
114
172
  fallbackTokenPresent: Boolean(fallback),
115
173
  runtimeTokenCached: Boolean(cachedToken),
116
174
  runtimeTokenExpiresInMs: cachedToken ? ttlMs : 0,
117
- lastAuthMode: describeGeniusAuthMode()
175
+ lastAuthMode: describeGeniusAuthMode(),
176
+ cachePath: TOKEN_CACHE_PATH,
177
+ cacheTokenPresent: false,
178
+ cacheTokenExpired: false,
179
+ cacheError: null
118
180
  };
181
+
182
+ try {
183
+ const raw = await fs.readFile(TOKEN_CACHE_PATH, 'utf8');
184
+ const parsed = JSON.parse(raw);
185
+ diagnostics.cacheTokenPresent = Boolean(parsed?.access_token);
186
+ if (parsed?.expires_at) {
187
+ diagnostics.cacheTokenExpired = Date.now() >= parsed.expires_at - 60_000;
188
+ }
189
+ } catch (error) {
190
+ diagnostics.cacheError = error?.code === 'ENOENT' ? null : (error?.message ?? null);
191
+ }
192
+
193
+ return diagnostics;
119
194
  }
@@ -11,7 +11,7 @@ const logger = createLogger('musixmatch-token-manager');
11
11
  // Only reliable when a persistent, writable filesystem is available
12
12
  // (i.e. local development). Ephemeral hosts (Render free tier, etc.)
13
13
  // may not have a writable FS, so the cache token is unavailable there.
14
- // • Fallback token — the token value supplied directly via MUSIXMATCH_USER_TOKEN or
14
+ // • Fallback token — the token value supplied directly via MUSIXMATCH_FALLBACK_TOKEN or
15
15
  // MUSIXMATCH_ALT_USER_TOKEN environment variables. This is the recommended
16
16
  // approach for production and remote deployments where the filesystem
17
17
  // cannot be relied upon for persistence.
@@ -60,7 +60,7 @@ async function writeCachedToken(token, desktopCookie) {
60
60
  if (!dirOk) {
61
61
  logger.warn(
62
62
  'Musixmatch token cache directory unavailable (read-only or restricted filesystem). ' +
63
- 'Token was NOT persisted to disk. Set MUSIXMATCH_USER_TOKEN as an environment variable ' +
63
+ 'Token was NOT persisted to disk. Set MUSIXMATCH_FALLBACK_TOKEN as an environment variable ' +
64
64
  'to ensure the token survives restarts in remote/ephemeral deployments.',
65
65
  { cachePath: TOKEN_CACHE_PATH }
66
66
  );
@@ -78,7 +78,7 @@ async function writeCachedToken(token, desktopCookie) {
78
78
  /**
79
79
  * Resolve the Musixmatch token using the following priority order:
80
80
  * 1. In-memory runtime cache (already resolved this session)
81
- * 2. MUSIXMATCH_USER_TOKEN env var — fallback token, first-priority env source
81
+ * 2. MUSIXMATCH_FALLBACK_TOKEN env var — fallback token, first-priority env source
82
82
  * 3. MUSIXMATCH_ALT_USER_TOKEN env var — fallback token, second-priority env source
83
83
  * 4. On-disk cache file — cache token, local dev only
84
84
  */
@@ -88,10 +88,10 @@ export async function getMusixmatchToken() {
88
88
  }
89
89
 
90
90
  // Prioritize env vars — these survive restarts on ephemeral hosts
91
- const userToken = getEnvValue('MUSIXMATCH_USER_TOKEN');
91
+ const userToken = getEnvValue('MUSIXMATCH_FALLBACK_TOKEN');
92
92
  if (userToken) {
93
93
  cachedToken = userToken;
94
- lastLoadedFrom = 'env:MUSIXMATCH_USER_TOKEN';
94
+ lastLoadedFrom = 'env:MUSIXMATCH_FALLBACK_TOKEN';
95
95
  cachedDesktopCookie = null;
96
96
  return cachedToken;
97
97
  }
@@ -129,7 +129,7 @@ export function describeMusixmatchTokenSource() {
129
129
  }
130
130
 
131
131
  export async function getMusixmatchTokenDiagnostics() {
132
- const userEnvToken = getEnvValue('MUSIXMATCH_USER_TOKEN');
132
+ const userEnvToken = getEnvValue('MUSIXMATCH_FALLBACK_TOKEN');
133
133
  const envToken = getEnvValue('MUSIXMATCH_ALT_USER_TOKEN');
134
134
 
135
135
  const diagnostics = {
@@ -161,7 +161,7 @@ export async function getMusixmatchTokenDiagnostics() {
161
161
  if (cachedToken) {
162
162
  diagnostics.resolvedSource = lastLoadedFrom;
163
163
  } else if (userEnvToken) {
164
- diagnostics.resolvedSource = 'env:MUSIXMATCH_USER_TOKEN';
164
+ diagnostics.resolvedSource = 'env:MUSIXMATCH_FALLBACK_TOKEN';
165
165
  } else if (envToken) {
166
166
  diagnostics.resolvedSource = 'env:MUSIXMATCH_ALT_USER_TOKEN';
167
167
  } else if (diagnostics.cacheTokenPresent) {