mr-magic-mcp-server 0.2.5 → 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/http-server.js +19 -6
- package/src/transport/mcp-http-server.js +22 -3
- 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/export-storage/redis-storage.js +5 -2
- 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/.env.example
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
# OAuth client_credentials endpoint at runtime and refreshes the token in
|
|
11
11
|
# memory automatically. No disk, no scripts, no manual token copying needed.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
# Set
|
|
13
|
+
# Direct token (env var) — static bearer token, no auto-refresh
|
|
14
|
+
# Set GENIUS_DIRECT_TOKEN. Works everywhere but the token must be
|
|
15
15
|
# manually updated on expiry (or redeploy with a new value).
|
|
16
16
|
# Use this only when client_credentials are unavailable.
|
|
17
17
|
#
|
|
@@ -23,28 +23,51 @@
|
|
|
23
23
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
24
24
|
GENIUS_CLIENT_ID= # Auto-refresh: OAuth client ID (enables runtime auto-refresh)
|
|
25
25
|
GENIUS_CLIENT_SECRET= # Auto-refresh: OAuth client secret (enables runtime auto-refresh)
|
|
26
|
-
|
|
26
|
+
GENIUS_DIRECT_TOKEN= # Direct token: static bearer token (used when client credentials are unavailable)
|
|
27
27
|
|
|
28
28
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
29
29
|
# REQUIRED (feature-specific) — Musixmatch
|
|
30
30
|
# At least one token source is needed for Musixmatch lyrics support.
|
|
31
31
|
# WARNING: Unauthorized Public API usage can result in a ban of your account!
|
|
32
32
|
# ───────────────────────────────────────────────────────────────────────────────
|
|
33
|
-
#
|
|
33
|
+
# Token resolution priority (1 = highest):
|
|
34
34
|
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
# free tier, serverless, containers without a mounted volume).
|
|
39
|
-
# Resolution order: MUSIXMATCH_FALLBACK_TOKEN (1st) → MUSIXMATCH_ALT_FALLBACK_TOKEN (2nd)
|
|
35
|
+
# 1. MUSIXMATCH_DIRECT_TOKEN (env var)
|
|
36
|
+
# Static bearer token. Works everywhere but must be manually updated on
|
|
37
|
+
# expiry. Use when no other option is available.
|
|
40
38
|
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
39
|
+
# 2. KV store — Upstash Redis or Cloudflare KV (recommended for ephemeral / npx)
|
|
40
|
+
# Run `npm run fetch:musixmatch-token` once to sign in and push the token
|
|
41
|
+
# to KV automatically. The server reads it from KV on startup.
|
|
42
|
+
# Works for npx installs and any deployment without a local filesystem.
|
|
43
|
+
# Configure with UPSTASH_REDIS_REST_URL/TOKEN (already used by the export
|
|
44
|
+
# backend) or the Cloudflare KV vars below.
|
|
45
|
+
#
|
|
46
|
+
# 3. On-disk cache token (local dev / persistent servers only)
|
|
47
|
+
# Run `npm run fetch:musixmatch-token` — writes .cache/musixmatch-token.json.
|
|
48
|
+
# The server reads it on startup when a writable filesystem is available.
|
|
45
49
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
MUSIXMATCH_DIRECT_TOKEN= # Direct token — set this in production / ephemeral deployments (highest priority env var)
|
|
51
|
+
|
|
52
|
+
# KV store for Musixmatch token persistence (ephemeral deployments / npx installs)
|
|
53
|
+
# Priority: Upstash Redis (priority 1) → Cloudflare KV (priority 2)
|
|
54
|
+
# If both are set, Upstash Redis is used and Cloudflare KV is ignored.
|
|
55
|
+
#
|
|
56
|
+
# Upstash Redis: reuse the same creds as the export backend (see below) — no extra setup.
|
|
57
|
+
# Cloudflare KV: set the three CF_* vars; create a namespace at dash.cloudflare.com.
|
|
58
|
+
CF_API_TOKEN= # Cloudflare API token with KV:Edit permission (CF KV only)
|
|
59
|
+
CF_ACCOUNT_ID= # Cloudflare account ID (CF KV only)
|
|
60
|
+
CF_KV_NAMESPACE_ID= # KV namespace ID to store the token in (CF KV only)
|
|
61
|
+
# Optional overrides (defaults are sensible for most users)
|
|
62
|
+
MUSIXMATCH_TOKEN_KV_KEY= # KV key name (default: mr-magic:musixmatch-token)
|
|
63
|
+
MUSIXMATCH_TOKEN_KV_TTL_SECONDS= # Token TTL in seconds (default: 2592000 = 30 days)
|
|
64
|
+
|
|
65
|
+
# Headless / Render deployment — push a pre-captured token without opening a browser.
|
|
66
|
+
# Set MUSIXMATCH_DIRECT_TOKEN to the full musixmatchUserToken JSON payload (from fetch:musixmatch-token output),
|
|
67
|
+
# then run `npm run push:musixmatch-token` or prepend it to the start command:
|
|
68
|
+
# npm run push:musixmatch-token && npm run server:mcp:http
|
|
69
|
+
# If unset, push:musixmatch-token exits silently (safe no-op in start commands).
|
|
70
|
+
# MUSIXMATCH_DIRECT_TOKEN also serves as the runtime token override (highest env priority).
|
|
48
71
|
|
|
49
72
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
50
73
|
# REQUIRED (feature-specific) — Airtable
|
|
@@ -68,6 +91,8 @@ MR_MAGIC_EXPORT_BACKEND= # local | inline | redis
|
|
|
68
91
|
MR_MAGIC_EXPORT_DIR= # Absolute path to the directory to write export files into
|
|
69
92
|
|
|
70
93
|
# Required when BACKEND=redis — get from https://console.upstash.com/redis/rest
|
|
94
|
+
# These same credentials are also used by the Musixmatch KV token store, so
|
|
95
|
+
# setting them once enables both features.
|
|
71
96
|
UPSTASH_REDIS_REST_URL=
|
|
72
97
|
UPSTASH_REDIS_REST_TOKEN=
|
|
73
98
|
|
|
@@ -93,6 +118,9 @@ MELON_COOKIE= # Pin a browser session cookie for consistent result
|
|
|
93
118
|
# Advanced / Debug (safe to leave unset in production)
|
|
94
119
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
95
120
|
|
|
121
|
+
# Playwright Headless mode toggle for fetch scripts that require browser automation (default: false)
|
|
122
|
+
HEADLESS=
|
|
123
|
+
|
|
96
124
|
# Override project root and .env file path resolution (rarely needed)
|
|
97
125
|
MR_MAGIC_ROOT=
|
|
98
126
|
MR_MAGIC_ENV_PATH=
|
package/README.md
CHANGED
|
@@ -37,28 +37,123 @@ automations, and CLI aficionados can all request lyrics from a single toolchain.
|
|
|
37
37
|
|
|
38
38
|
### Quick start — npx (no clone required)
|
|
39
39
|
|
|
40
|
-
The easiest way to use Mr. Magic
|
|
41
|
-
|
|
40
|
+
The easiest way to use Mr. Magic is via `npx`. No clone or local install needed —
|
|
41
|
+
the package is fetched from npm on first run and cached locally.
|
|
42
|
+
|
|
43
|
+
#### MCP stdio server (local MCP clients — Cline, Claude Desktop, etc.)
|
|
42
44
|
|
|
43
45
|
```bash
|
|
44
46
|
npx -y mr-magic-mcp-server
|
|
45
47
|
```
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
Credentials are passed via the `env` block in your MCP client config (see
|
|
50
|
+
[MCP Client Configuration](#mcp-client-configuration)).
|
|
51
|
+
|
|
52
|
+
#### MCP Streamable HTTP server (remote / browser-based MCP clients)
|
|
53
|
+
|
|
54
|
+
The Streamable HTTP server is the correct choice for TypingMind, any browser-based
|
|
55
|
+
client, or whenever you're hosting Mr. Magic on a remote machine.
|
|
48
56
|
|
|
49
57
|
```bash
|
|
50
|
-
|
|
58
|
+
# Streamable HTTP — listens on port 3444, endpoint: /mcp
|
|
59
|
+
GENIUS_DIRECT_TOKEN=your_token \
|
|
60
|
+
MUSIXMATCH_DIRECT_TOKEN=your_token \
|
|
61
|
+
npx -y --package mr-magic-mcp-server mcp-http-server
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Connect your client to `http://localhost:3444/mcp` (or your public URL + `/mcp`).
|
|
65
|
+
|
|
66
|
+
The same server also exposes the **legacy SSE** endpoints for older clients:
|
|
67
|
+
- `GET /sse` — opens the event stream
|
|
68
|
+
- `POST /messages?sessionId=...` — sends JSON-RPC messages
|
|
69
|
+
|
|
70
|
+
Both protocols run on the same port simultaneously — no extra config needed.
|
|
71
|
+
|
|
72
|
+
#### JSON HTTP automation server
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# JSON HTTP — listens on port 3333, endpoint: POST /
|
|
76
|
+
GENIUS_DIRECT_TOKEN=your_token \
|
|
77
|
+
npx -y --package mr-magic-mcp-server http-server
|
|
51
78
|
```
|
|
52
79
|
|
|
53
|
-
|
|
80
|
+
#### Global install (binaries always on PATH)
|
|
54
81
|
|
|
55
82
|
```bash
|
|
56
|
-
|
|
57
|
-
|
|
83
|
+
npm install -g mr-magic-mcp-server
|
|
84
|
+
|
|
85
|
+
mcp-server # MCP stdio server
|
|
86
|
+
mcp-http-server # Streamable HTTP MCP server (+ legacy SSE on same port)
|
|
58
87
|
http-server # JSON HTTP automation server
|
|
59
88
|
mrmagic-cli --help # CLI
|
|
60
89
|
```
|
|
61
90
|
|
|
91
|
+
#### Musixmatch token for npx / ephemeral / headless installs
|
|
92
|
+
|
|
93
|
+
When running via `npx`, on Render free tier, or on any server without a browser or
|
|
94
|
+
persistent filesystem, the Musixmatch token cannot be captured via Playwright there.
|
|
95
|
+
The workflow is:
|
|
96
|
+
|
|
97
|
+
1. **Capture the token locally** (one-time on any machine with a browser):
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
git clone https://github.com/mrnajiboy/mr-magic-mcp-server.git
|
|
101
|
+
cd mr-magic-mcp-server && npm install
|
|
102
|
+
npm run fetch:musixmatch-token
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
After signing in, the script prints the full token JSON payload.
|
|
106
|
+
Copy the entire printed JSON object (the `MUSIXMATCH_DIRECT_TOKEN=...` line).
|
|
107
|
+
|
|
108
|
+
2. **Push to KV** so the server can read it on every cold start.
|
|
109
|
+
Set up Upstash Redis (free tier at [console.upstash.com](https://console.upstash.com/redis))
|
|
110
|
+
and run the push script with KV credentials. No browser needed:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
UPSTASH_REDIS_REST_URL=https://xxx.upstash.io \
|
|
114
|
+
UPSTASH_REDIS_REST_TOKEN=your_upstash_token \
|
|
115
|
+
MUSIXMATCH_DIRECT_TOKEN='<paste token JSON here>' \
|
|
116
|
+
npm run push:musixmatch-token
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
3. **Start the server** with the same Upstash credentials — it reads the token from
|
|
120
|
+
KV on every cold start (no `MUSIXMATCH_DIRECT_TOKEN` needed when KV is configured):
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
GENIUS_DIRECT_TOKEN=... \
|
|
124
|
+
UPSTASH_REDIS_REST_URL=https://xxx.upstash.io \
|
|
125
|
+
UPSTASH_REDIS_REST_TOKEN=your_upstash_token \
|
|
126
|
+
npx -y --package mr-magic-mcp-server mcp-http-server
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
4. Re-run steps 1–2 only when the Musixmatch token expires (typically ~30 days).
|
|
130
|
+
|
|
131
|
+
#### Musixmatch on Render (headless, no SSH)
|
|
132
|
+
|
|
133
|
+
On Render free tier you cannot SSH in or open a browser. The recommended pattern is:
|
|
134
|
+
|
|
135
|
+
1. Run `npm run fetch:musixmatch-token` locally, copy the token JSON from the output.
|
|
136
|
+
|
|
137
|
+
2. In the Render Dashboard → **Environment** tab, set:
|
|
138
|
+
- `MUSIXMATCH_DIRECT_TOKEN` = `<your token JSON>` *(used as both push source and runtime override)*
|
|
139
|
+
- `UPSTASH_REDIS_REST_URL` = your Upstash endpoint
|
|
140
|
+
- `UPSTASH_REDIS_REST_TOKEN` = your Upstash token
|
|
141
|
+
|
|
142
|
+
3. Set the Render **Start Command** to:
|
|
143
|
+
```
|
|
144
|
+
npm run push:musixmatch-token && npm run server:mcp:http
|
|
145
|
+
```
|
|
146
|
+
On every (re)start, the token is pushed to Upstash then the server reads it
|
|
147
|
+
from KV. If `MUSIXMATCH_DIRECT_TOKEN` is unset the push step is a silent no-op.
|
|
148
|
+
|
|
149
|
+
4. When the token expires: update `MUSIXMATCH_DIRECT_TOKEN` from a fresh local
|
|
150
|
+
`fetch:musixmatch-token` run, trigger a redeploy on Render. Done.
|
|
151
|
+
|
|
152
|
+
> **Genius on ephemeral hosts:** Genius does not need this flow.
|
|
153
|
+
> Set `GENIUS_CLIENT_ID` + `GENIUS_CLIENT_SECRET` instead — the server calls the
|
|
154
|
+
> Genius OAuth `client_credentials` endpoint at runtime, auto-refreshes the token
|
|
155
|
+
> in memory, and never needs a browser, a KV store, or a captured session token.
|
|
156
|
+
|
|
62
157
|
### Local repo (development / contribution)
|
|
63
158
|
|
|
64
159
|
1. Clone or download the repository:
|
|
@@ -111,23 +206,36 @@ grouped below by purpose.
|
|
|
111
206
|
| ---------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
112
207
|
| `GENIUS_CLIENT_ID` | OAuth client ID for auto-refresh (recommended). Get from [genius.com/api-clients](https://genius.com/api-clients). |
|
|
113
208
|
| `GENIUS_CLIENT_SECRET` | OAuth client secret for auto-refresh (recommended). |
|
|
114
|
-
| `
|
|
209
|
+
| `GENIUS_DIRECT_TOKEN` | Static direct bearer token. Used when client credentials are unavailable. |
|
|
115
210
|
|
|
116
211
|
Token resolution order (first match wins):
|
|
117
212
|
|
|
118
213
|
1. In-memory runtime cache
|
|
119
214
|
2. Auto-refresh via `GENIUS_CLIENT_ID` + `GENIUS_CLIENT_SECRET` ← **recommended**
|
|
120
|
-
3. `
|
|
121
|
-
4.
|
|
215
|
+
3. `GENIUS_DIRECT_TOKEN` env var (static, no auto-refresh)
|
|
216
|
+
4. KV store — Upstash Redis or Cloudflare KV (written automatically by auto-refresh)
|
|
217
|
+
5. On-disk `.cache/genius-token.json` (local dev only)
|
|
122
218
|
|
|
123
219
|
### Musixmatch credentials
|
|
124
220
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
221
|
+
Token resolution order (first match wins):
|
|
222
|
+
|
|
223
|
+
1. **Env var** — `MUSIXMATCH_DIRECT_TOKEN`
|
|
224
|
+
2. **KV store** — Upstash Redis (priority 1) or Cloudflare KV (priority 2)
|
|
225
|
+
3. **On-disk cache** — `.cache/musixmatch-token.json` (local dev / persistent servers)
|
|
226
|
+
|
|
227
|
+
| Variable | Description |
|
|
228
|
+
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
229
|
+
| `MUSIXMATCH_DIRECT_TOKEN` | Static bearer token. Recommended for production / ephemeral hosts. Also used as push source by `push:musixmatch-token`. |
|
|
230
|
+
| `UPSTASH_REDIS_REST_URL` | Upstash Redis KV backend URL. Also used by the export backend — set once, used for both. |
|
|
231
|
+
| `UPSTASH_REDIS_REST_TOKEN` | Upstash Redis KV bearer token. Takes precedence over Cloudflare KV when both are set. |
|
|
232
|
+
| `CF_API_TOKEN` | Cloudflare API token with `KV:Edit` permission (Cloudflare KV backend). |
|
|
233
|
+
| `CF_ACCOUNT_ID` | Cloudflare account ID (Cloudflare KV backend). |
|
|
234
|
+
| `CF_KV_NAMESPACE_ID` | Cloudflare KV namespace ID (Cloudflare KV backend). |
|
|
235
|
+
| `MUSIXMATCH_TOKEN_KV_KEY` | KV key name for the token store. Default: `mr-magic:musixmatch-token`. |
|
|
236
|
+
| `MUSIXMATCH_TOKEN_KV_TTL_SECONDS` | Token TTL in the KV store (seconds). Default: `2592000` (30 days). |
|
|
237
|
+
| `MUSIXMATCH_TOKEN_CACHE` | Path to the on-disk cache file. Default: `.cache/musixmatch-token.json`. |
|
|
238
|
+
| `MUSIXMATCH_AUTO_FETCH` | Set to `1` to attempt headless token re-fetch when no token is found. |
|
|
131
239
|
|
|
132
240
|
### Export and storage
|
|
133
241
|
|
|
@@ -173,7 +281,7 @@ Genius credentials are resolved in this order — the first available source win
|
|
|
173
281
|
refreshed in memory. **Recommended for all deployments**, including Render and
|
|
174
282
|
ephemeral hosts. No disk, no scripts, no manual token copying.
|
|
175
283
|
|
|
176
|
-
2. **
|
|
284
|
+
2. **Direct token** (`GENIUS_DIRECT_TOKEN`) — a static bearer token. Works
|
|
177
285
|
everywhere but does not auto-refresh. Update by redeploying with a new value.
|
|
178
286
|
|
|
179
287
|
3. **Cache token** (`.cache/genius-token.json`) — written by `npm run fetch:genius-token`.
|
|
@@ -185,9 +293,8 @@ Musixmatch uses a captured browser session token. There is no OAuth callback.
|
|
|
185
293
|
|
|
186
294
|
**For production / ephemeral hosts (Render, containers):**
|
|
187
295
|
|
|
188
|
-
Set `
|
|
189
|
-
|
|
190
|
-
when the filesystem may be wiped between restarts.
|
|
296
|
+
Set `MUSIXMATCH_DIRECT_TOKEN` directly in your environment. This is the highest-priority
|
|
297
|
+
env var option and the only reliable one when the filesystem may be wiped between restarts.
|
|
191
298
|
|
|
192
299
|
**For local development:**
|
|
193
300
|
|
|
@@ -205,11 +312,11 @@ The workflow:
|
|
|
205
312
|
3. After the redirect to `https://www.musixmatch.com/discover`, the script prints
|
|
206
313
|
the captured token and writes the cache file.
|
|
207
314
|
4. **For remote deployments:** copy the `token` value from the printed JSON and set
|
|
208
|
-
it as `
|
|
315
|
+
it as `MUSIXMATCH_DIRECT_TOKEN` in your platform environment. Do **not** rely on
|
|
209
316
|
the cache file surviving restarts on ephemeral hosts.
|
|
210
317
|
|
|
211
318
|
> **Developer accounts:** Get API access from [developer.musixmatch.com](https://developer.musixmatch.com)
|
|
212
|
-
> and set the resulting token as `
|
|
319
|
+
> and set the resulting token as `MUSIXMATCH_DIRECT_TOKEN`.
|
|
213
320
|
>
|
|
214
321
|
> **Public accounts:** Visit [auth.musixmatch.com](https://auth.musixmatch.com), sign in,
|
|
215
322
|
> and capture the token using the script above.
|
|
@@ -309,7 +416,7 @@ Recommended Render service settings:
|
|
|
309
416
|
|
|
310
417
|
- **Start Command:** `npm run server:mcp:http`
|
|
311
418
|
- **Environment:** set provider credentials (`GENIUS_CLIENT_ID`, `GENIUS_CLIENT_SECRET`,
|
|
312
|
-
`
|
|
419
|
+
`MUSIXMATCH_DIRECT_TOKEN`, etc.) in the Render Dashboard → Environment tab
|
|
313
420
|
- **Health Check Path:** `/health` (returns `{ "status": "ok", "providers": [...] }`)
|
|
314
421
|
|
|
315
422
|
> For custom domains, add them to `MR_MAGIC_ALLOWED_HOSTS` (comma-separated) in
|
|
@@ -615,8 +722,8 @@ Works with any local MCP client that supports `command` / `args`:
|
|
|
615
722
|
"command": "npx",
|
|
616
723
|
"args": ["-y", "mr-magic-mcp-server"],
|
|
617
724
|
"env": {
|
|
618
|
-
"
|
|
619
|
-
"
|
|
725
|
+
"GENIUS_DIRECT_TOKEN": "...",
|
|
726
|
+
"MUSIXMATCH_DIRECT_TOKEN": "...",
|
|
620
727
|
"AIRTABLE_PERSONAL_ACCESS_TOKEN": "..."
|
|
621
728
|
}
|
|
622
729
|
}
|
|
@@ -1055,8 +1162,8 @@ npm run server:mcp:http # Streamable HTTP MCP — port 3444
|
|
|
1055
1162
|
|
|
1056
1163
|
- **LRCLIB** — Public API with synced lyric coverage. No auth required.
|
|
1057
1164
|
- **Genius** — Requires `GENIUS_CLIENT_ID` + `GENIUS_CLIENT_SECRET` (auto-refresh,
|
|
1058
|
-
recommended) or `
|
|
1059
|
-
- **Musixmatch** — Requires a token. Use `
|
|
1165
|
+
recommended) or `GENIUS_DIRECT_TOKEN` (static direct token).
|
|
1166
|
+
- **Musixmatch** — Requires a token. Use `MUSIXMATCH_DIRECT_TOKEN` for production /
|
|
1060
1167
|
ephemeral hosts; use the on-disk cache token (`npm run fetch:musixmatch-token`) for
|
|
1061
1168
|
local dev. See [Musixmatch](#musixmatch) for the full workflow.
|
|
1062
1169
|
- **Melon** — Works anonymously. Set `MELON_COOKIE` for pinned / reproducible sessions.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mr-magic-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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,8 @@
|
|
|
30
30
|
"format": "prettier --write .",
|
|
31
31
|
"format:check": "prettier --check .",
|
|
32
32
|
"test": "node src/tests/run-tests.js",
|
|
33
|
-
"fetch:musixmatch-token": "node src/scripts/
|
|
33
|
+
"fetch:musixmatch-token": "node src/scripts/fetch_musixmatch_token.mjs",
|
|
34
|
+
"push:musixmatch-token": "node src/scripts/push_musixmatch_token.mjs",
|
|
34
35
|
"fetch:genius-token": "node src/scripts/fetch_genius_token.mjs",
|
|
35
36
|
"repro:mcp:arg-boundary": "node src/scripts/mcp-arg-boundary-repro.mjs",
|
|
36
37
|
"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,133 @@ 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(' The server reads MUSIXMATCH_DIRECT_TOKEN on startup (highest priority env var):\n');
|
|
69
|
+
console.log(` MUSIXMATCH_DIRECT_TOKEN=${tokenString}\n`);
|
|
70
|
+
|
|
40
71
|
console.log('─'.repeat(68) + '\n');
|
|
41
72
|
}
|
|
42
73
|
|
|
74
|
+
function isHeadlessEnabled() {
|
|
75
|
+
const value = (process.env.HEADLESS || '').trim().toLowerCase();
|
|
76
|
+
return value === '1' || value === 'true' || value === 'yes';
|
|
77
|
+
}
|
|
78
|
+
|
|
43
79
|
async function main() {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
80
|
+
const headless = isHeadlessEnabled();
|
|
81
|
+
|
|
82
|
+
// Persistent browser session — stores cookies/logins between script runs so you don't
|
|
83
|
+
// have to sign in again until your session actually expires.
|
|
84
|
+
// Override with PLAYWRIGHT_SESSION_DIR env var if you need a different location.
|
|
85
|
+
const sessionDir =
|
|
86
|
+
process.env.PLAYWRIGHT_SESSION_DIR || path.resolve('.cache', 'playwright-session');
|
|
87
|
+
await mkdir(sessionDir, { recursive: true });
|
|
88
|
+
|
|
89
|
+
console.log(`Launching Playwright (headless=${headless}) to acquire Musixmatch token...`);
|
|
90
|
+
console.log(`Browser session directory: ${sessionDir}\n`);
|
|
91
|
+
|
|
92
|
+
// Try real installed browsers in priority order so Google OAuth doesn't block the
|
|
93
|
+
// automated bundled Chromium. Override with BROWSER=<name> to skip straight to one.
|
|
94
|
+
// Chromium channels : chrome, brave, msedge, comet
|
|
95
|
+
// Other engines : firefox, safari (webkit)
|
|
96
|
+
// Last resort : bundled Chromium (may be blocked by Google OAuth)
|
|
97
|
+
//
|
|
98
|
+
// launchPersistentContext() is used instead of launch() + newContext() so the browser
|
|
99
|
+
// session (cookies, logins) is saved to sessionDir and reused on subsequent runs.
|
|
100
|
+
const CHROMIUM_UA =
|
|
101
|
+
'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';
|
|
102
|
+
const chromiumArgs = ['--disable-blink-features=AutomationControlled'];
|
|
103
|
+
const baseOpts = { headless, slowMo: headless ? 0 : 150, viewport: { width: 1280, height: 900 } };
|
|
104
|
+
const chromiumOpts = { ...baseOpts, args: chromiumArgs, userAgent: CHROMIUM_UA };
|
|
105
|
+
|
|
106
|
+
// Each launcher returns a BrowserContext (launchPersistentContext skips browser.newContext()).
|
|
107
|
+
const candidates = [
|
|
108
|
+
['chrome', () => chromium.launchPersistentContext(sessionDir, { ...chromiumOpts, channel: 'chrome' })],
|
|
109
|
+
['brave (channel)', () => chromium.launchPersistentContext(sessionDir, { ...chromiumOpts, channel: 'brave' })],
|
|
110
|
+
['brave (path)', () => chromium.launchPersistentContext(sessionDir, { ...chromiumOpts, executablePath: '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser' })],
|
|
111
|
+
['msedge', () => chromium.launchPersistentContext(sessionDir, { ...chromiumOpts, channel: 'msedge' })],
|
|
112
|
+
['comet', () => chromium.launchPersistentContext(sessionDir, { ...chromiumOpts, executablePath: '/Applications/Comet.app/Contents/MacOS/Comet' })],
|
|
113
|
+
['firefox', () => firefox.launchPersistentContext(sessionDir, { ...baseOpts })],
|
|
114
|
+
['safari (webkit)', () => webkit.launchPersistentContext(sessionDir, { ...baseOpts })],
|
|
115
|
+
['bundled chromium',() => chromium.launchPersistentContext(sessionDir, { ...chromiumOpts })],
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
// If BROWSER is set, move that candidate to the front.
|
|
119
|
+
const browserEnv = (process.env.BROWSER || '').trim().toLowerCase();
|
|
120
|
+
const orderedCandidates = browserEnv
|
|
121
|
+
? [
|
|
122
|
+
...candidates.filter(([label]) => label.startsWith(browserEnv)),
|
|
123
|
+
...candidates.filter(([label]) => !label.startsWith(browserEnv)),
|
|
124
|
+
]
|
|
125
|
+
: candidates;
|
|
126
|
+
|
|
127
|
+
let context;
|
|
128
|
+
let chosenLabel;
|
|
129
|
+
for (const [label, launcher] of orderedCandidates) {
|
|
130
|
+
try {
|
|
131
|
+
context = await launcher();
|
|
132
|
+
chosenLabel = label;
|
|
133
|
+
break;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.warn(` ${label} not available (${err.message?.split('\n')[0]}), trying next...`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!context) {
|
|
140
|
+
console.error('No usable browser found. Install Chrome, Brave, Edge, Firefox, or Safari.');
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
console.log(`Using browser: ${chosenLabel}`);
|
|
144
|
+
|
|
145
|
+
// Remove the webdriver flag that Google uses to detect automated browsers.
|
|
146
|
+
await context.addInitScript(() => {
|
|
147
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
148
|
+
});
|
|
149
|
+
|
|
47
150
|
const page = await context.newPage();
|
|
48
151
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
152
|
+
page.on('console', (msg) => {
|
|
153
|
+
if (msg.type() === 'error') {
|
|
154
|
+
const text = msg.text();
|
|
155
|
+
// Suppress benign COOP warning emitted by the auth page itself.
|
|
156
|
+
if (text.includes('Cross-Origin-Opener-Policy')) return;
|
|
157
|
+
console.error(`[browser console error] ${text}`);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
page.on('pageerror', (err) => console.error(`[browser page error] ${err.message}`));
|
|
161
|
+
|
|
162
|
+
console.log(`Navigating to ${AUTH_URL} — sign in in the browser window that appears.`);
|
|
163
|
+
// 'commit' fires as soon as the server response starts (before content loads), which avoids
|
|
164
|
+
// ERR_ABORTED on browsers like Comet that intercept or redirect during initial navigation.
|
|
165
|
+
await page.goto(AUTH_URL, { waitUntil: 'commit' });
|
|
166
|
+
console.log('Waiting to be redirected to https://account.musixmatch.com/ ...');
|
|
167
|
+
await page.waitForURL('https://account.musixmatch.com/**', { timeout: 0 });
|
|
53
168
|
|
|
54
|
-
const cookies = await context.cookies('https://
|
|
169
|
+
const cookies = await context.cookies('https://account.musixmatch.com');
|
|
55
170
|
const userCookie = cookies.find((cookie) => cookie.name === 'musixmatchUserToken');
|
|
56
171
|
const desktopCookie = cookies.find((cookie) => cookie.name === 'web-desktop-app-v1.0');
|
|
57
172
|
if (!userCookie) {
|
|
@@ -70,14 +185,20 @@ async function main() {
|
|
|
70
185
|
console.log('\nMusixmatch token payload:');
|
|
71
186
|
console.log(JSON.stringify(parsed, null, 2));
|
|
72
187
|
|
|
73
|
-
|
|
188
|
+
const decodedDesktopCookie = desktopCookie ? decodeURIComponent(desktopCookie.value) : null;
|
|
189
|
+
|
|
190
|
+
// Write to all configured storage backends in parallel.
|
|
191
|
+
await Promise.allSettled([
|
|
192
|
+
saveToken(parsed, decodedDesktopCookie),
|
|
193
|
+
saveToKv(parsed, decodedDesktopCookie),
|
|
194
|
+
]);
|
|
74
195
|
|
|
75
196
|
// Extract the raw token string for the deployment hint.
|
|
76
197
|
// The parsed payload is the full musixmatchUserToken JSON object; the server
|
|
77
198
|
// stores and reads the entire parsed object as the `token` field.
|
|
78
199
|
printDeploymentBlock(parsed);
|
|
79
200
|
|
|
80
|
-
await
|
|
201
|
+
await context.close();
|
|
81
202
|
}
|
|
82
203
|
|
|
83
204
|
main().catch((error) => {
|