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.
- package/.env.example +43 -15
- package/README.md +134 -27
- package/package.json +3 -2
- package/src/providers/genius.js +1 -1
- package/src/providers/musixmatch.js +2 -3
- package/src/scripts/fetch_genius_token.mjs +21 -4
- package/src/scripts/fetch_musixmatch_token.mjs +144 -23
- package/src/scripts/push_musixmatch_token.mjs +131 -0
- package/src/transport/mcp-tools.js +2 -3
- package/src/transport/token-startup-log.js +4 -5
- package/src/utils/config.js +1 -1
- package/src/utils/kv-store.js +157 -0
- package/src/utils/tokens/genius-token-manager.js +97 -22
- package/src/utils/tokens/musixmatch-token-manager.js +77 -36
|
@@ -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
|
-
// •
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// •
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
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.
|
|
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
|
|
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.
|
|
82
|
-
* 3.
|
|
83
|
-
* 4. On-disk cache file
|
|
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
|
-
//
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
cachedToken =
|
|
94
|
-
lastLoadedFrom = 'env:
|
|
131
|
+
// 2. Direct token from env var — survives 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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
144
|
-
|
|
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 (
|
|
164
|
-
diagnostics.resolvedSource = 'env:
|
|
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 {
|