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/.env.example +53 -24
- package/README.md +439 -143
- package/package.json +4 -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 +169 -23
- package/src/scripts/push_musixmatch_token.mjs +133 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mr-magic-mcp-server",
|
|
3
|
-
"version": "0.
|
|
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/
|
|
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"
|
package/src/providers/genius.js
CHANGED
|
@@ -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
|
|
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
|
|
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 '../
|
|
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 (
|
|
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
|
|
23
|
-
console.log(`
|
|
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 '../
|
|
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(`\
|
|
21
|
-
console.log('(
|
|
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
|
-
|
|
32
|
-
console.log('
|
|
33
|
-
console.log('
|
|
34
|
-
console.log('
|
|
35
|
-
console.log('
|
|
36
|
-
console.log('
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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://
|
|
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
|
-
|
|
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
|
|
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
|
-
'
|
|
438
|
+
'GENIUS_DIRECT_TOKEN',
|
|
439
439
|
'GENIUS_CLIENT_ID',
|
|
440
440
|
'GENIUS_CLIENT_SECRET',
|
|
441
|
-
'
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/utils/config.js
CHANGED
|
@@ -23,7 +23,7 @@ export function getEnvValue(name) {
|
|
|
23
23
|
return process.env[name] ?? null;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const DEFAULT_REQUIRED = ['
|
|
26
|
+
const DEFAULT_REQUIRED = ['GENIUS_DIRECT_TOKEN'];
|
|
27
27
|
const warnedMissingEnvCache = new Set();
|
|
28
28
|
|
|
29
29
|
function getMissingEnvVars(requiredVars = DEFAULT_REQUIRED) {
|