mr-magic-mcp-server 0.1.5

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.
Files changed (41) hide show
  1. package/.env.example +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1118 -0
  4. package/package.json +75 -0
  5. package/prompts/airtable-song-importer.md +239 -0
  6. package/src/bin/cli.js +9 -0
  7. package/src/bin/http-server.js +7 -0
  8. package/src/bin/mcp-http-server.js +7 -0
  9. package/src/bin/mcp-server.js +6 -0
  10. package/src/core/export.js +83 -0
  11. package/src/core/find-service.js +66 -0
  12. package/src/core/formatting.js +39 -0
  13. package/src/core/preview.js +54 -0
  14. package/src/index.js +138 -0
  15. package/src/provider-result-schema.js +63 -0
  16. package/src/providers/genius.js +250 -0
  17. package/src/providers/lrclib.js +73 -0
  18. package/src/providers/melon.js +201 -0
  19. package/src/providers/musixmatch.js +165 -0
  20. package/src/services/airtable-writer.js +246 -0
  21. package/src/services/lyrics-service.js +372 -0
  22. package/src/tools/cli.js +695 -0
  23. package/src/transport/http-server.js +151 -0
  24. package/src/transport/mcp-http-server.js +202 -0
  25. package/src/transport/mcp-response.js +44 -0
  26. package/src/transport/mcp-server.js +77 -0
  27. package/src/transport/mcp-tools.js +513 -0
  28. package/src/transport/token-startup-log.js +133 -0
  29. package/src/transport/tool-args.js +113 -0
  30. package/src/utils/config.js +57 -0
  31. package/src/utils/export-storage/inline-storage.js +7 -0
  32. package/src/utils/export-storage/local-storage.js +39 -0
  33. package/src/utils/export-storage/redis-storage.js +36 -0
  34. package/src/utils/export-storage/shared-redis-client.js +69 -0
  35. package/src/utils/export-storage.js +29 -0
  36. package/src/utils/logger.js +74 -0
  37. package/src/utils/lyrics-format.js +170 -0
  38. package/src/utils/slugify.js +18 -0
  39. package/src/utils/storage-cache.js +29 -0
  40. package/src/utils/tokens/genius-token-manager.js +119 -0
  41. package/src/utils/tokens/musixmatch-token-manager.js +130 -0
@@ -0,0 +1,119 @@
1
+ import axios from 'axios';
2
+
3
+ import { getEnvValue } from '../config.js';
4
+ import { createLogger } from '../logger.js';
5
+
6
+ const GENIUS_TOKEN_ENDPOINT = 'https://api.genius.com/oauth/token';
7
+ const logger = createLogger('genius-token-manager');
8
+
9
+ let cachedToken = null;
10
+ let cachedExpiry = 0;
11
+ let lastAuthMode = 'unknown';
12
+
13
+ function getFallbackToken() {
14
+ return getEnvValue('GENIUS_ACCESS_TOKEN');
15
+ }
16
+
17
+ function tokenExpired() {
18
+ if (!cachedToken) return true;
19
+ const now = Date.now();
20
+ return now >= cachedExpiry - 60_000; // refresh one minute early
21
+ }
22
+
23
+ async function fetchClientCredentialsToken() {
24
+ const clientId = getEnvValue('GENIUS_CLIENT_ID');
25
+ const clientSecret = getEnvValue('GENIUS_CLIENT_SECRET');
26
+ if (!clientId || !clientSecret) {
27
+ return null;
28
+ }
29
+ const params = new URLSearchParams({
30
+ client_id: clientId,
31
+ client_secret: clientSecret,
32
+ grant_type: 'client_credentials'
33
+ });
34
+ try {
35
+ const response = await axios.post(GENIUS_TOKEN_ENDPOINT, params.toString(), {
36
+ headers: {
37
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
38
+ }
39
+ });
40
+ const { access_token: accessToken, expires_in: expiresIn } = response.data ?? {};
41
+ if (!accessToken) {
42
+ throw new Error('Genius token response missing access_token');
43
+ }
44
+ const ttl = Number(expiresIn) || 3600;
45
+ cachedToken = accessToken;
46
+ cachedExpiry = Date.now() + ttl * 1000;
47
+ lastAuthMode = 'client_credentials';
48
+ logger.info('Genius token refreshed', { ttlSeconds: ttl });
49
+ return cachedToken;
50
+ } catch (error) {
51
+ logger.error('Failed to refresh Genius token', {
52
+ error: error.response?.data || error.message
53
+ });
54
+ return null;
55
+ }
56
+ }
57
+
58
+ export async function getGeniusToken({ forceRefresh = false } = {}) {
59
+ if (!forceRefresh && !tokenExpired()) {
60
+ return cachedToken;
61
+ }
62
+ const token = await fetchClientCredentialsToken();
63
+ if (token) {
64
+ return token;
65
+ }
66
+ const fallback = getFallbackToken();
67
+ if (fallback && fallback !== cachedToken) {
68
+ logger.warn('Using fallback Genius access token from environment');
69
+ cachedToken = fallback;
70
+ cachedExpiry = Date.now() + 86_400_000; // 1 day placeholder
71
+ lastAuthMode = 'env_access_token';
72
+ return cachedToken;
73
+ }
74
+ return cachedToken;
75
+ }
76
+
77
+ export function invalidateGeniusToken() {
78
+ cachedToken = null;
79
+ cachedExpiry = 0;
80
+ lastAuthMode = 'unknown';
81
+ }
82
+
83
+ export function hasValidGeniusAuth() {
84
+ const hasClient = Boolean(getEnvValue('GENIUS_CLIENT_ID') && getEnvValue('GENIUS_CLIENT_SECRET'));
85
+ if (hasClient) return true;
86
+ return Boolean(getFallbackToken());
87
+ }
88
+
89
+ export function describeGeniusAuthMode() {
90
+ if (lastAuthMode !== 'unknown') {
91
+ return lastAuthMode;
92
+ }
93
+ if (cachedToken && cachedExpiry > Date.now()) {
94
+ return 'cached_runtime_token';
95
+ }
96
+ const hasClient = Boolean(getEnvValue('GENIUS_CLIENT_ID') && getEnvValue('GENIUS_CLIENT_SECRET'));
97
+ if (hasClient) {
98
+ return 'client_credentials';
99
+ }
100
+ if (getFallbackToken()) {
101
+ return 'env_access_token';
102
+ }
103
+ return 'none';
104
+ }
105
+
106
+ export function getGeniusDiagnostics() {
107
+ const clientId = getEnvValue('GENIUS_CLIENT_ID');
108
+ const clientSecret = getEnvValue('GENIUS_CLIENT_SECRET');
109
+ const fallback = getFallbackToken();
110
+ const ttlMs = Math.max(cachedExpiry - Date.now(), 0);
111
+
112
+ return {
113
+ clientCredentialsPresent: Boolean(clientId && clientSecret),
114
+ fallbackTokenPresent: Boolean(fallback),
115
+ runtimeTokenCached: Boolean(cachedToken),
116
+ runtimeTokenExpiresInMs: cachedToken ? ttlMs : 0,
117
+ lastAuthMode: describeGeniusAuthMode()
118
+ };
119
+ }
@@ -0,0 +1,130 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { getEnvValue, getProjectRoot } from '../config.js';
5
+ import { createLogger } from '../logger.js';
6
+
7
+ const logger = createLogger('musixmatch-token-manager');
8
+ const TOKEN_CACHE_PATH =
9
+ process.env.MUSIXMATCH_TOKEN_CACHE ||
10
+ path.join(getProjectRoot(), '.cache', 'musixmatch-token.json');
11
+
12
+ let cachedToken = null;
13
+ let lastLoadedFrom = 'unknown';
14
+ let cachedDesktopCookie = null;
15
+
16
+ function getCacheDir() {
17
+ return path.dirname(TOKEN_CACHE_PATH);
18
+ }
19
+
20
+ async function ensureCacheDir() {
21
+ const dir = getCacheDir();
22
+ await fs.mkdir(dir, { recursive: true }).catch(() => {});
23
+ }
24
+
25
+ async function readCachedToken() {
26
+ try {
27
+ await ensureCacheDir();
28
+ const raw = await fs.readFile(TOKEN_CACHE_PATH, 'utf8');
29
+ const parsed = JSON.parse(raw);
30
+ if (parsed?.token) {
31
+ cachedToken = parsed.token;
32
+ cachedDesktopCookie = parsed.desktopCookie || null;
33
+ lastLoadedFrom = 'cache';
34
+ return cachedToken;
35
+ }
36
+ } catch (error) {
37
+ // ignore missing cache
38
+ }
39
+ return null;
40
+ }
41
+
42
+ async function writeCachedToken(token, desktopCookie) {
43
+ if (!token) return;
44
+ try {
45
+ await ensureCacheDir();
46
+ const payload = { token };
47
+ if (desktopCookie) {
48
+ payload.desktopCookie = desktopCookie;
49
+ }
50
+ await fs.writeFile(TOKEN_CACHE_PATH, JSON.stringify(payload, null, 2), 'utf8');
51
+ } catch (error) {
52
+ logger.warn('Failed to persist Musixmatch token cache', { error });
53
+ }
54
+ }
55
+
56
+ export async function getMusixmatchToken() {
57
+ if (cachedToken) {
58
+ return cachedToken;
59
+ }
60
+ const envToken = getEnvValue('MUSIXMATCH_TOKEN');
61
+ if (envToken) {
62
+ cachedToken = envToken;
63
+ lastLoadedFrom = 'env';
64
+ cachedDesktopCookie = null;
65
+ return cachedToken;
66
+ }
67
+ return readCachedToken();
68
+ }
69
+
70
+ export async function setMusixmatchToken(token, { desktopCookie } = {}) {
71
+ if (!token) return;
72
+ cachedToken = token;
73
+ lastLoadedFrom = 'runtime';
74
+ cachedDesktopCookie = desktopCookie || null;
75
+ await writeCachedToken(token, desktopCookie);
76
+ logger.info('Musixmatch token updated', {
77
+ source: 'runtime',
78
+ desktopCookiePresent: Boolean(desktopCookie)
79
+ });
80
+ }
81
+
82
+ export function invalidateMusixmatchToken() {
83
+ cachedToken = null;
84
+ }
85
+
86
+ export function describeMusixmatchTokenSource() {
87
+ return lastLoadedFrom;
88
+ }
89
+
90
+ export async function getMusixmatchTokenDiagnostics() {
91
+ const envToken = getEnvValue('MUSIXMATCH_TOKEN');
92
+ const diagnostics = {
93
+ cachePath: TOKEN_CACHE_PATH,
94
+ cacheDir: getCacheDir(),
95
+ cacheAttempted: false,
96
+ cacheFound: false,
97
+ cacheBytes: 0,
98
+ cacheTokenPresent: false,
99
+ cacheError: null,
100
+ envPresent: Boolean(envToken),
101
+ runtimeTokenCached: Boolean(cachedToken),
102
+ lastLoadedFrom,
103
+ resolvedSource: 'none'
104
+ };
105
+
106
+ try {
107
+ await ensureCacheDir();
108
+ diagnostics.cacheAttempted = true;
109
+ const raw = await fs.readFile(TOKEN_CACHE_PATH);
110
+ diagnostics.cacheFound = true;
111
+ diagnostics.cacheBytes = raw.length;
112
+ const parsed = JSON.parse(raw.toString('utf8'));
113
+ diagnostics.cacheTokenPresent = Boolean(parsed?.token);
114
+ } catch (error) {
115
+ diagnostics.cacheError = error?.code === 'ENOENT' ? null : error?.message;
116
+ }
117
+
118
+ if (cachedToken) {
119
+ diagnostics.resolvedSource = lastLoadedFrom;
120
+ } else if (envToken) {
121
+ diagnostics.resolvedSource = 'env';
122
+ } else if (diagnostics.cacheTokenPresent) {
123
+ diagnostics.resolvedSource = 'cache';
124
+ } else {
125
+ diagnostics.resolvedSource = 'none';
126
+ }
127
+
128
+ diagnostics.tokenPresent = diagnostics.resolvedSource !== 'none';
129
+ return diagnostics;
130
+ }