Added an MCP server and a SKILL.md for agentic use of the tools
parent
a89f278c24
commit
240a88a671
@ -0,0 +1,92 @@
|
||||
# Spotify Top Tracks CLI
|
||||
|
||||
Find the most popular songs for any artist, or scrape a concert/festival webpage to discover artists and their top tracks.
|
||||
|
||||
## Working directory
|
||||
|
||||
```
|
||||
/Users/oabrivard/Projects/javascript/spotify
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### 1. Top tracks for a single artist
|
||||
|
||||
```bash
|
||||
node get_popularity.js "Artist Name"
|
||||
node get_popularity.js "Artist Name" --top 5
|
||||
```
|
||||
|
||||
Output (stdout, JSON):
|
||||
```json
|
||||
{
|
||||
"artist": "Radiohead",
|
||||
"tracks": [
|
||||
{ "name": "Creep", "album": "Pablo Honey", "uri": "spotify:track:..." },
|
||||
{ "name": "No Surprises", "album": "OK Computer", "uri": "spotify:track:..." },
|
||||
{ "name": "Let Down", "album": "OK Computer", "uri": "spotify:track:..." }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Scrape a URL and get top tracks for each artist
|
||||
|
||||
```bash
|
||||
node get_popularity.js --url "https://www.example-festival.com/lineup" --llm anthropic
|
||||
node get_popularity.js --url "https://www.example-festival.com/lineup" --llm openai --top 5
|
||||
```
|
||||
|
||||
Output (stdout, JSON):
|
||||
```json
|
||||
{
|
||||
"artists": [
|
||||
{
|
||||
"artist": "The Cure",
|
||||
"tracks": [
|
||||
{ "name": "Friday I'm in Love", "album": "Wish", "uri": "spotify:track:..." }
|
||||
]
|
||||
},
|
||||
{
|
||||
"artist": "Radiohead",
|
||||
"tracks": [
|
||||
{ "name": "Creep", "album": "Pablo Honey", "uri": "spotify:track:..." }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Scrape + create a Spotify playlist
|
||||
|
||||
```bash
|
||||
node get_popularity.js --url "https://www.example-festival.com/lineup" --llm anthropic --create_playlist
|
||||
```
|
||||
|
||||
Adds a `playlist` field to the output:
|
||||
```json
|
||||
{
|
||||
"artists": [ ... ],
|
||||
"playlist": {
|
||||
"name": "www.example-festival.com_lineup",
|
||||
"url": "https://open.spotify.com/playlist/..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: `--create_playlist` requires browser-based Spotify OAuth authorization.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Required | Default | Description |
|
||||
|---|---|---|---|
|
||||
| (positional) | No | "Radiohead" | Artist name (single artist mode) |
|
||||
| `--url` | No | - | URL to scrape for artist names |
|
||||
| `--top N` | No | 3 | Number of top tracks per artist |
|
||||
| `--llm` | No | anthropic | LLM provider for scraping (`anthropic` or `openai`) |
|
||||
| `--create_playlist` | No | - | Create a Spotify playlist (requires `--url`) |
|
||||
|
||||
## Notes
|
||||
|
||||
- Progress messages go to stderr; JSON result goes to stdout
|
||||
- Use `2>/dev/null` to suppress progress messages
|
||||
- The Spotify API is rate-limited to 2 requests per second
|
||||
@ -0,0 +1,133 @@
|
||||
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-spotify]", ...args);
|
||||
|
||||
const server = new McpServer({
|
||||
name: "spotify-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);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue