@pedrocid/music-mcp
Version:
MCP server for controlling Apple Music on macOS (v1.0.5)
236 lines (235 loc) • 9.3 kB
JavaScript
"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}`);
}
}