YouTube Music MCP Server

#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'; import axios from 'axios'; import { exec, ExecException } from 'child_process'; import { z } from 'zod'; const API_KEY = process.env.YOUTUBE_API_KEY; if (!API_KEY) { throw new Error('YOUTUBE_API_KEY environment variable is required'); } interface PlaySongArgs { song_name?: string; artist_name?: string; } const isValidPlaySongArgs = (args: unknown): args is PlaySongArgs => { const obj = args as Record<string, unknown>; return ( typeof args === 'object' && args !== null && (obj['song_name'] === undefined || typeof obj['song_name'] === 'string') && (obj['artist_name'] === undefined || typeof obj['artist_name'] === 'string') ); }; interface SearchResult { title: string; videoId: string; description: string; } class YoutubeMusicServer { private server: Server; preferences: Record<string, unknown>; log: string[]; private youtubeApi: ReturnType<typeof axios.create>; constructor() { this.server = new Server( { name: 'youtube-music-server', version: '0.2.0', }, { capabilities: { resources: {}, tools: {}, }, } ); this.preferences = {}; this.log = []; this.youtubeApi = axios.create({ baseURL: 'https://www.googleapis.com/youtube/v3', params: { key: API_KEY, part: 'snippet', maxResults: 5, type: 'video', }, }); this.setupToolHandlers(); this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'play_song', description: 'Play a song on Youtube Music', inputSchema: { type: 'object', properties: { song_name: { type: 'string', description: 'Name of the song to play', }, artist_name: { type: 'string', description: 'Name of the artist', }, }, required: ['song_name'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== 'play_song') { throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } if (!isValidPlaySongArgs(request.params.arguments)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid play_song arguments' ); } const { song_name, artist_name } = request.params.arguments; const searchQuery = artist_name ? `${song_name} ${artist_name}` : song_name; this.log.push(`Searching for song: ${searchQuery}`); try { const response = await this.youtubeApi.get('/search', { params: { q: searchQuery, }, }); const searchResults = response.data.items.map((item: { snippet: { title: string; description: string }; id: { videoId: string }; }): SearchResult => ({ title: item.snippet.title, videoId: item.id.videoId, description: item.snippet.description, })); console.error(`Search results: ${JSON.stringify(searchResults, null, 2)}`); if (searchResults.length > 0) { const topResultVideoId = searchResults[0].videoId; const youtubeMusicUrl = `https://music.youtube.com/watch?v=${topResultVideoId}`; const appleScript = ` tell application "Google Chrome" activate open location "${youtubeMusicUrl}" end tell `; exec(`osascript -e '${appleScript}'`, (error: ExecException | null, stdout: string, stderr: string) => { if (error) { this.log.push(`Error opening song in Chrome: ${error.message}`); throw new McpError( ErrorCode.InternalError, `Error opening song in Chrome: ${error.message}` ); } else { this.log.push(`Opened song in Chrome: ${youtubeMusicUrl}`); } }); return { content: [ { type: 'text', text: `Playing top result: ${searchResults[0].title}` } ] }; } else { return { content: [ { type: 'text', text: `No search results found for: ${searchQuery}` } ] }; } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); this.log.push(`Error searching for song: ${errorMessage}`); throw new McpError( ErrorCode.InternalError, `Error searching for song: ${errorMessage}` ); } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Youtube Music MCP server running on stdio'); } } const server = new YoutubeMusicServer(); server.run().catch(console.error);