UNPKG

@pedrocid/music-mcp

Version:

MCP server for controlling Apple Music on macOS (v1.0.5)

236 lines (235 loc) 9.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.searchMusicTool = exports.getMusicInfoTool = void 0; exports.handleGetMusicInfo = handleGetMusicInfo; exports.handleSearchMusic = handleSearchMusic; const logger_js_1 = require("../logger.js"); const config_js_1 = require("../config.js"); const child_process_1 = require("child_process"); const path_1 = require("path"); exports.getMusicInfoTool = { name: 'get_music_info', description: 'Retrieve information about current playback or library', inputSchema: { type: 'object', properties: { infoType: { type: 'string', enum: ['current_track', 'playback_status', 'queue', 'library_stats'], description: 'Type of information to retrieve' }, format: { type: 'string', enum: ['simple', 'detailed'], description: 'Output format detail level' } }, required: ['infoType'] } }; exports.searchMusicTool = { name: 'search_music', description: 'Search the music library', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string' }, searchType: { type: 'string', enum: ['all', 'track', 'album', 'artist', 'playlist'], description: 'Type of content to search for' }, limit: { type: 'number', minimum: 1, maximum: 100, description: 'Maximum number of results to return' } }, required: ['query'] } }; async function handleGetMusicInfo(input) { logger_js_1.logger.info({ infoType: input.infoType }, 'Getting music info'); const config = (0, config_js_1.getConfig)(); try { let result; switch (input.infoType) { case 'current_track': case 'playback_status': result = await executeAppleScript((0, path_1.join)(__dirname, '../scripts/library/get-current-track.applescript'), [], config.timeoutSeconds * 1000); break; case 'library_stats': { // For library stats, we'll get playlists and albums const playlistsResult = await executeAppleScript((0, path_1.join)(__dirname, '../scripts/library/get-playlists.applescript'), [], config.timeoutSeconds * 1000); const albumsResult = await executeAppleScript((0, path_1.join)(__dirname, '../scripts/library/get-albums.applescript'), [], config.timeoutSeconds * 1000); result = JSON.stringify({ playlists: JSON.parse(playlistsResult), albums: JSON.parse(albumsResult) }); break; } case 'queue': // For queue info, we'll return current track info for now // (Apple Music's queue access is limited via AppleScript) result = await executeAppleScript((0, path_1.join)(__dirname, '../scripts/library/get-current-track.applescript'), [], config.timeoutSeconds * 1000); break; default: return { success: false, message: `Unknown info type: ${input.infoType}`, error: 'Invalid info type' }; } if (result.startsWith('Error')) { return { success: false, message: result, error: result }; } // Try to parse JSON result let data; try { data = JSON.parse(result); } catch { // If not JSON, return as string data = result; } return { success: true, data, message: 'Music info retrieved successfully' }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger_js_1.logger.error({ error, infoType: input.infoType }, 'Failed to get music info'); return { success: false, message: `Failed to retrieve music info: ${errorMessage}`, error: errorMessage }; } } async function handleSearchMusic(input) { logger_js_1.logger.info({ query: input.query, searchType: input.searchType }, 'Searching music'); const config = (0, config_js_1.getConfig)(); const limit = Math.min(input.limit || config.maxSearchResults, 100); try { if (!input.query.trim()) { return { success: false, message: 'Search query cannot be empty', error: 'Empty query' }; } let result; switch (input.searchType || 'all') { case 'all': case 'track': result = await executeAppleScript((0, path_1.join)(__dirname, '../scripts/library/search-tracks.applescript'), [input.query], config.timeoutSeconds * 1000); break; case 'playlist': result = await executeAppleScript((0, path_1.join)(__dirname, '../scripts/library/get-playlists.applescript'), [], config.timeoutSeconds * 1000); // Filter playlists by search query try { const playlists = JSON.parse(result); const filtered = playlists.filter((playlist) => playlist.name.toLowerCase().includes(input.query.toLowerCase())); result = JSON.stringify(filtered.slice(0, limit)); } catch { // If parsing fails, return original result } break; case 'album': result = await executeAppleScript((0, path_1.join)(__dirname, '../scripts/library/get-albums.applescript'), [], config.timeoutSeconds * 1000); // Filter albums by search query try { const albums = JSON.parse(result); const filtered = albums.filter((album) => album.album.toLowerCase().includes(input.query.toLowerCase()) || album.artist.toLowerCase().includes(input.query.toLowerCase())); result = JSON.stringify(filtered.slice(0, limit)); } catch { // If parsing fails, return original result } break; case 'artist': result = await executeAppleScript((0, path_1.join)(__dirname, '../scripts/library/get-albums.applescript'), [], config.timeoutSeconds * 1000); // Filter by artist try { const albums = JSON.parse(result); const filtered = albums.filter((album) => album.artist.toLowerCase().includes(input.query.toLowerCase())); result = JSON.stringify(filtered.slice(0, limit)); } catch { // If parsing fails, return original result } break; default: return { success: false, message: `Unknown search type: ${input.searchType}`, error: 'Invalid search type' }; } if (result.startsWith('Error')) { return { success: false, message: result, error: result }; } // Try to parse JSON result let data; try { data = JSON.parse(result); // Apply limit if it's an array if (Array.isArray(data)) { data = data.slice(0, limit); } } catch { // If not JSON, return as string data = result; } return { success: true, data, message: `Found ${Array.isArray(data) ? data.length : 1} result(s)` }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger_js_1.logger.error({ error, query: input.query }, 'Music search failed'); return { success: false, message: `Search failed: ${errorMessage}`, error: errorMessage }; } } async function executeAppleScript(scriptPath, args = [], timeout = 30000) { try { // Build the command with properly escaped arguments const quotedArgs = args.map(arg => `"${arg.replace(/"/g, '\\"')}"`).join(' '); const command = `osascript "${scriptPath}" ${quotedArgs}`; const result = (0, child_process_1.execSync)(command, { timeout, encoding: 'utf8', stdio: 'pipe' }); return result.toString().trim(); } catch (error) { if (error.code === 'TIMEOUT') { throw new Error(`AppleScript execution timed out after ${timeout}ms`); } throw new Error(`AppleScript execution failed: ${error.message}`); } }