mr-magic-mcp-server 0.2.6 → 0.3.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.
@@ -3,20 +3,28 @@ import path from 'node:path';
3
3
 
4
4
  import { getEnvValue, getProjectRoot } from '../config.js';
5
5
  import { createLogger } from '../logger.js';
6
+ import { describeKvBackend, isKvConfigured, kvGet, kvSet } from '../kv-store.js';
6
7
 
7
8
  const logger = createLogger('musixmatch-token-manager');
8
9
 
9
10
  // Token source terminology used throughout this module:
10
- // • Cache token — loaded from the on-disk cache file written by the fetch script.
11
- // Only reliable when a persistent, writable filesystem is available
12
- // (i.e. local development). Ephemeral hosts (Render free tier, etc.)
13
- // may not have a writable FS, so the cache token is unavailable there.
14
- // • Fallback token the token value supplied directly via MUSIXMATCH_FALLBACK_TOKEN or
15
- // MUSIXMATCH_ALT_USER_TOKEN environment variables. This is the recommended
16
- // approach for production and remote deployments where the filesystem
17
- // cannot be relied upon for persistence.
11
+ // • Direct token — MUSIXMATCH_DIRECT_TOKEN env var. A static bearer token set
12
+ // directly in the environment. Recommended for production and
13
+ // remote deployments where the filesystem cannot be relied upon
14
+ // for persistence. Highest priority after in-memory cache.
15
+ // • KV token stored in a remote KV store (Upstash Redis or Cloudflare KV).
16
+ // Ideal for ephemeral/serverless deployments and npx installs
17
+ // where there is no local filesystem at all.
18
+ // Cache token — loaded from the on-disk cache file written by the fetch script.
19
+ // Only reliable when a persistent, writable filesystem is available
20
+ // (i.e. local development). Ephemeral hosts (Render free tier, etc.)
21
+ // may not have a writable FS, so the cache token is unavailable there.
22
+
23
+ // KV key and TTL — configurable via env vars.
24
+ const KV_KEY = process.env.MUSIXMATCH_TOKEN_KV_KEY || 'mr-magic:musixmatch-token';
25
+ const KV_TTL_SECONDS = parseInt(process.env.MUSIXMATCH_TOKEN_KV_TTL_SECONDS || '2592000', 10); // 30 days
18
26
  const TOKEN_CACHE_PATH =
19
- process.env.MUSIXMATCH_ALT_USER_TOKEN_CACHE ||
27
+ process.env.MUSIXMATCH_TOKEN_CACHE ||
20
28
  path.join(getProjectRoot(), '.cache', 'musixmatch-token.json');
21
29
 
22
30
  let cachedToken = null;
@@ -60,7 +68,7 @@ async function writeCachedToken(token, desktopCookie) {
60
68
  if (!dirOk) {
61
69
  logger.warn(
62
70
  'Musixmatch token cache directory unavailable (read-only or restricted filesystem). ' +
63
- 'Token was NOT persisted to disk. Set MUSIXMATCH_FALLBACK_TOKEN as an environment variable ' +
71
+ 'Token was NOT persisted to disk. Set MUSIXMATCH_DIRECT_TOKEN as an environment variable ' +
64
72
  'to ensure the token survives restarts in remote/ephemeral deployments.',
65
73
  { cachePath: TOKEN_CACHE_PATH }
66
74
  );
@@ -75,36 +83,65 @@ async function writeCachedToken(token, desktopCookie) {
75
83
  }
76
84
  }
77
85
 
86
+ // ─── KV store helpers ─────────────────────────────────────────────────────────
87
+
88
+ async function readKvToken() {
89
+ if (!isKvConfigured()) return null;
90
+ try {
91
+ const raw = await kvGet(KV_KEY);
92
+ if (!raw) return null;
93
+ const parsed = JSON.parse(raw);
94
+ if (parsed?.token) {
95
+ cachedToken = parsed.token;
96
+ cachedDesktopCookie = parsed.desktopCookie || null;
97
+ lastLoadedFrom = `kv:${describeKvBackend()}`;
98
+ return cachedToken;
99
+ }
100
+ } catch {
101
+ // KV read error — not fatal; fall through to disk cache
102
+ }
103
+ return null;
104
+ }
105
+
106
+ async function writeKvToken(token, desktopCookie) {
107
+ if (!isKvConfigured() || !token) return;
108
+ try {
109
+ const payload = JSON.stringify({ token, ...(desktopCookie ? { desktopCookie } : {}) });
110
+ await kvSet(KV_KEY, payload, KV_TTL_SECONDS);
111
+ } catch (error) {
112
+ logger.warn('Failed to persist Musixmatch token to KV store', {
113
+ backend: describeKvBackend(),
114
+ error: error?.message
115
+ });
116
+ }
117
+ }
118
+
78
119
  /**
79
120
  * Resolve the Musixmatch token using the following priority order:
80
121
  * 1. In-memory runtime cache (already resolved this session)
81
- * 2. MUSIXMATCH_FALLBACK_TOKEN env var fallback token, first-priority env source
82
- * 3. MUSIXMATCH_ALT_USER_TOKEN env var fallback token, second-priority env source
83
- * 4. On-disk cache file cache token, local dev only
122
+ * 2. MUSIXMATCH_DIRECT_TOKEN env var direct/static bearer token (highest env priority)
123
+ * 3. KV store Upstash Redis or Cloudflare KV (ephemeral/npx)
124
+ * 4. On-disk cache file — local dev / persistent server only
84
125
  */
85
126
  export async function getMusixmatchToken() {
86
127
  if (cachedToken) {
87
128
  return cachedToken;
88
129
  }
89
130
 
90
- // Prioritize env varsthese survive restarts on ephemeral hosts
91
- const userToken = getEnvValue('MUSIXMATCH_FALLBACK_TOKEN');
92
- if (userToken) {
93
- cachedToken = userToken;
94
- lastLoadedFrom = 'env:MUSIXMATCH_FALLBACK_TOKEN';
131
+ // 2. Direct token from env varsurvives restarts on ephemeral hosts without any external service
132
+ const directToken = getEnvValue('MUSIXMATCH_DIRECT_TOKEN');
133
+ if (directToken) {
134
+ cachedToken = directToken;
135
+ lastLoadedFrom = 'env:MUSIXMATCH_DIRECT_TOKEN';
95
136
  cachedDesktopCookie = null;
96
137
  return cachedToken;
97
138
  }
98
139
 
99
- const envToken = getEnvValue('MUSIXMATCH_ALT_USER_TOKEN');
100
- if (envToken) {
101
- cachedToken = envToken;
102
- lastLoadedFrom = 'env:MUSIXMATCH_ALT_USER_TOKEN';
103
- cachedDesktopCookie = null;
104
- return cachedToken;
105
- }
140
+ // 3. KV store — ideal for ephemeral deployments and npx installs with no local filesystem
141
+ const kvToken = await readKvToken();
142
+ if (kvToken) return kvToken;
106
143
 
107
- // Fall back to disk cache for local development
144
+ // 4. On-disk cache local dev / persistent hosts with a writable filesystem
108
145
  return readCachedToken();
109
146
  }
110
147
 
@@ -113,10 +150,16 @@ export async function setMusixmatchToken(token, { desktopCookie } = {}) {
113
150
  cachedToken = token;
114
151
  lastLoadedFrom = 'runtime';
115
152
  cachedDesktopCookie = desktopCookie || null;
116
- await writeCachedToken(token, desktopCookie);
153
+ // Write to both storage backends in parallel; failures are logged, not thrown.
154
+ await Promise.allSettled([
155
+ writeCachedToken(token, desktopCookie),
156
+ writeKvToken(token, desktopCookie)
157
+ ]);
117
158
  logger.info('Musixmatch token updated', {
118
159
  source: 'runtime',
119
- desktopCookiePresent: Boolean(desktopCookie)
160
+ desktopCookiePresent: Boolean(desktopCookie),
161
+ kvConfigured: isKvConfigured(),
162
+ kvBackend: describeKvBackend()
120
163
  });
121
164
  }
122
165
 
@@ -129,8 +172,7 @@ export function describeMusixmatchTokenSource() {
129
172
  }
130
173
 
131
174
  export async function getMusixmatchTokenDiagnostics() {
132
- const userEnvToken = getEnvValue('MUSIXMATCH_FALLBACK_TOKEN');
133
- const envToken = getEnvValue('MUSIXMATCH_ALT_USER_TOKEN');
175
+ const directEnvToken = getEnvValue('MUSIXMATCH_DIRECT_TOKEN');
134
176
 
135
177
  const diagnostics = {
136
178
  cachePath: TOKEN_CACHE_PATH,
@@ -140,8 +182,9 @@ export async function getMusixmatchTokenDiagnostics() {
140
182
  cacheBytes: 0,
141
183
  cacheTokenPresent: false,
142
184
  cacheError: null,
143
- userEnvPresent: Boolean(userEnvToken),
144
- envPresent: Boolean(envToken),
185
+ directEnvPresent: Boolean(directEnvToken),
186
+ kvConfigured: isKvConfigured(),
187
+ kvBackend: describeKvBackend(),
145
188
  runtimeTokenCached: Boolean(cachedToken),
146
189
  lastLoadedFrom,
147
190
  resolvedSource: 'none'
@@ -160,10 +203,8 @@ export async function getMusixmatchTokenDiagnostics() {
160
203
 
161
204
  if (cachedToken) {
162
205
  diagnostics.resolvedSource = lastLoadedFrom;
163
- } else if (userEnvToken) {
164
- diagnostics.resolvedSource = 'env:MUSIXMATCH_FALLBACK_TOKEN';
165
- } else if (envToken) {
166
- diagnostics.resolvedSource = 'env:MUSIXMATCH_ALT_USER_TOKEN';
206
+ } else if (directEnvToken) {
207
+ diagnostics.resolvedSource = 'env:MUSIXMATCH_DIRECT_TOKEN';
167
208
  } else if (diagnostics.cacheTokenPresent) {
168
209
  diagnostics.resolvedSource = 'cache';
169
210
  } else {