const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js"); const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js"); const { z } = require("zod"); const { getAccessToken, getTopTracks, scrapeArtists, LLM_PROVIDERS, } = require("./get_popularity"); const log = (...args) => console.error("[mcp-song-popularity]", ...args); const server = new McpServer({ name: "song-popularity-tools", version: "1.0.0", }); // Tool 1: get_top_tracks server.tool( "get_top_tracks", "Get the top N most popular tracks for a given artist on Spotify", { artistName: z.string().describe("The artist or band name to search for"), topN: z.number().optional().default(3).describe("Number of top tracks to return (default: 3)"), }, async ({ artistName, topN }) => { try { log(`get_top_tracks: "${artistName}", topN=${topN}`); const token = await getAccessToken(); const result = await getTopTracks(token, artistName, topN); if (!result.artist) { return { content: [{ type: "text", text: `Artist "${artistName}" not found on Spotify.` }], }; } return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } catch (err) { return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true, }; } } ); // Tool 2: scrape_artists_from_url server.tool( "scrape_artists_from_url", "Scrape a web page (concerts, festivals, music venues) and extract artist names using an LLM", { url: z.string().url().describe("URL of the page to scrape for artist names"), provider: z .enum(Object.keys(LLM_PROVIDERS)) .optional() .default("anthropic") .describe("LLM provider to use for extraction (default: anthropic)"), }, async ({ url, provider }) => { try { log(`scrape_artists_from_url: "${url}", provider=${provider}`); const artists = await scrapeArtists(url, provider, log); return { content: [{ type: "text", text: JSON.stringify(artists, null, 2) }], }; } catch (err) { return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true, }; } } ); // Tool 3: get_top_songs_from_url server.tool( "get_top_songs_from_url", "Scrape a web page to find artists, then return the top N songs for each artist from Spotify", { url: z.string().url().describe("URL of the page to scrape for artist names"), topN: z.number().optional().default(3).describe("Number of top songs per artist (default: 3)"), provider: z .enum(Object.keys(LLM_PROVIDERS)) .optional() .default("anthropic") .describe("LLM provider to use for extraction (default: anthropic)"), }, async ({ url, topN, provider }) => { try { log(`get_top_songs_from_url: "${url}", topN=${topN}, provider=${provider}`); const artistNames = await scrapeArtists(url, provider, log); if (artistNames.length === 0) { return { content: [{ type: "text", text: "No artist names found on the page." }], }; } log(`Found ${artistNames.length} artist(s), looking up tracks...`); const token = await getAccessToken(); const results = {}; for (const name of artistNames) { const result = await getTopTracks(token, name, topN); if (result.artist) { results[result.artist] = result.tracks; } else { log(`Artist "${name}" not found on Spotify, skipping.`); } } return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }], }; } catch (err) { return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true, }; } } ); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); log("Spotify MCP server started"); } main().catch((err) => { log("Fatal error:", err); process.exit(1); });