#
tokens: 16813/50000 11/11 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .DS_Store
├── .gitignore
├── build
│   ├── spotify-mcp-sse.js
│   └── spotify-mcp.js
├── Dockerfile
├── mcp
│   ├── spotify-mcp-http.js
│   ├── spotify-mcp-oauth-http.js
│   ├── spotify-mcp-sse.ts
│   └── spotify-mcp.ts
├── package.json
├── Readme.md
├── smithery.yaml
├── spotify-auth.js
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
node_modules
secrets.json
package-lock.json
.env
```

--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------

```markdown
# Spotify MCP Server

A simple Model Context Protocol (MCP) server that lets you interact with Spotify through Claude. This server enables Claude to search for songs, create playlists, get recommendations, and more using your Spotify account.

## Features

- Search for tracks on Spotify
- View your Spotify profile
- Create playlists
- Add tracks to playlists
- Get personalized music recommendations

## Tools Available

| Tool Name                  | Description                                              |
| -------------------------- | -------------------------------------------------------- |
| `set-spotify-credentials`  | Set your Spotify authentication credentials              |
| `check-credentials-status` | Check if your credentials are valid and who is logged in |
| `search-tracks`            | Search for tracks by name, artist, or keywords           |
| `get-current-user`         | Get your Spotify profile information                     |
| `create-playlist`          | Create a new playlist on your account                    |
| `add-tracks-to-playlist`   | Add tracks to an existing playlist                       |
| `get-recommendations`      | Get recommendations based on seed tracks                 |

## Setup Instructions

## Plug and Play - HTTP Spotify MCP Server

1. Go to [Claude AI](https://claude.ai/)
2. Click on Add Connectors
3. Click on Manage Connectors
4. Add custom connector
5. Add the https server link
6. Mail [email protected] to whitelist your spotify ID/mail
7. That's it. Your claude is ready to use Spotify. You will be prompted to sign-in using spotify OAuth.

## Run Spotify MCP locally

### 1. Prerequisites

- Node.js v16 or higher
- npm
- A Spotify account
- A registered Spotify Developer application

### 2. Create a Spotify Developer App

1. Go to [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/)
2. Log in with your Spotify account
3. Click "Create an App"
4. Fill in the app name and description
5. Add `http://localhost:8888/callback` as a Redirect URI
6. Note your Client ID and Client Secret

### 3. Install the Project

```bash
# Clone or download the project first
cd spotify-mcp-server

# Install dependencies
npm install
```

### 4. Get Your Spotify Tokens

Edit the `spotify-auth.js` file to include your Client ID and Client Secret:

```javascript
// Replace these with your Spotify app credentials
const CLIENT_ID = "your_client_id_here";
const CLIENT_SECRET = "your_client_secret_here";
```

Then run the authentication script:

```bash
node spotify-auth.js
```

This will:

1. Open a URL in your browser
2. Prompt you to log in to Spotify
3. Ask for your permission to access your account
4. Save the tokens to `secrets.json`

### 5. Build the MCP Server

```bash
npm run build
```

### 6. Configure Claude Desktop

Edit your Claude Desktop configuration file:

- On macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- On Windows: `%APPDATA%\Claude\claude_desktop_config.json`

Add the following configuration:

```json
{
  "mcpServers": {
    "spotify": {
      "command": "node",
      "args": ["/full/path/to/spotify-mcp-server/build/spotify-mcp-server.js"]
    }
  }
}
```

Replace `/full/path/to/spotify-mcp-server` with the actual path to your project directory.

### 7. Restart Claude Desktop

Close and reopen Claude Desktop to load the new configuration.

## Usage

When you start a conversation with Claude, you'll first need to set your Spotify credentials:

1. Look at your `secrets.json` file to get your credentials
2. Use the `set-spotify-credentials` tool to authenticate
3. Then use any of the other Spotify tools

## Example Prompts

### Setting Up Credentials

```
I want to connect to my Spotify account. Here are my credentials from secrets.json:

Tool: set-spotify-credentials
Parameters:
{
  "clientId": "your_client_id",
  "clientSecret": "your_client_secret",
  "accessToken": "your_access_token",
  "refreshToken": "your_refresh_token"
}
```

### Basic Commands

Check your account:

```
Can you check who I'm logged in as on Spotify?

Tool: get-current-user
Parameters: {}
```

Search for tracks:

```
Search for songs by Weekend

Tool: search-tracks
Parameters:
{
  "query": "Taylor Swift",
  "limit": 5
}
```

Create a playlist:

```
Create a new playlist called "My Pretty pretty girlfriend"

Tool: create-playlist
Parameters:
{
  "name": "My Pretty pretty girlfriend",
  "description": "For my girlfriend. Created with Claude and the Spotify MCP server"
}
```

### Multi-Step Tasks

Creating a playlist with songs:

```
I want to create a workout playlist with energetic songs. First, search for some high-energy songs. Then create a playlist called "Workout Mix" and add those songs to it.
```

Getting recommendations based on favorites:

```
I like the song "Blinding Lights" by The Weeknd. Can you search for it, then find similar songs, and create a playlist with those recommendations?
```

## Troubleshooting

- **Error: No access token available**: You need to set your credentials first using the `set-spotify-credentials` tool
- **Authentication failures**: Your tokens may have expired. Run the auth script again to get fresh tokens
- **Invalid credentials**: Double check that you're using the correct Client ID and Client Secret

## Notes

- The server stores credentials in memory only
- You'll need to set credentials each time you start a new conversation
- If Claude Desktop restarts, you'll need to set credentials again

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./mcp",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["*.ts", "mcp/spotify-mcp.ts", "mcp/spotify-mcp-sse.ts"],
  "exclude": ["node_modules"]
}

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
FROM node:lts-alpine

WORKDIR /app

# Copy package.json and lock file if available
COPY package.json package-lock.json* ./

# Install dependencies without running scripts
RUN npm install --ignore-scripts

# Copy source files
COPY . .

# Build the TypeScript code
RUN npm run build

CMD [ "node", "build/spotify-mcp-server.js" ]

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "spotify-mcp-server",
  "version": "1.0.0",
  "description": "MCP server for Spotify API integration",
  "type": "module",
  "main": "build/spotify-mcp.js",
  "scripts": {
    "auth": "node spotify-auth.js",
    "build": "tsc",
    "start": "node build/spotify-mcp-http.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.16.0",
    "@types/cors": "^2.8.18",
    "@types/express": "^5.0.1",
    "axios": "^1.6.2",
    "cors": "^2.8.5",
    "express": "^4.21.2",
    "node-fetch": "^3.3.2",
    "zod": "^3.24.2"
  },
  "devDependencies": {
    "@types/node": "^22.14.0",
    "typescript": "^5.8.3"
  }
}

```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    required: []
    properties:
      clientId:
        type: string
        description: Spotify Client ID
      clientSecret:
        type: string
        description: Spotify Client Secret
      accessToken:
        type: string
        description: Spotify Access Token
      refreshToken:
        type: string
        description: Spotify Refresh Token
  commandFunction:
    # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
    |-
    (config) => {
      const env = {};
      if (config.clientId) { env.SPOTIFY_CLIENT_ID = config.clientId; }
      if (config.clientSecret) { env.SPOTIFY_CLIENT_SECRET = config.clientSecret; }
      if (config.accessToken) { env.SPOTIFY_ACCESS_TOKEN = config.accessToken; }
      if (config.refreshToken) { env.SPOTIFY_REFRESH_TOKEN = config.refreshToken; }
      return {
        command: "node",
        args: ["build/spotify-mcp-server.js"],
        env: env
      };
    }
  exampleConfig:
    clientId: your-spotify-client-id
    clientSecret: your-spotify-client-secret
    accessToken: your-initial-access-token
    refreshToken: your-initial-refresh-token

```

--------------------------------------------------------------------------------
/spotify-auth.js:
--------------------------------------------------------------------------------

```javascript
import express from "express";
import axios from "axios";
import fs from "fs";

// Replace these with your Spotify app credentials
const CLIENT_ID = process.env.SPOTIFY_CLIENT_ID;
const CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET;
const REDIRECT_URI = process.env.SPOTIFY_REDIRECT_URI;

const app = express();
const PORT = 8888;

app.get("/login", (req, res) => {
  res.redirect(
    `https://accounts.spotify.com/authorize?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${REDIRECT_URI}&scope=user-read-private%20user-read-email%20playlist-read-private%20playlist-modify-private%20playlist-modify-public`
  );
});

app.get("/callback", async (req, res) => {
  const code = req.query.code;

  try {
    const tokenResponse = await axios({
      method: "post",
      url: "https://accounts.spotify.com/api/token",
      params: {
        code: code,
        redirect_uri: REDIRECT_URI,
        grant_type: "authorization_code",
      },
      headers: {
        Authorization:
          "Basic " +
          Buffer.from(CLIENT_ID + ":" + CLIENT_SECRET).toString("base64"),
        "Content-Type": "application/x-www-form-urlencoded",
      },
    });

    // Save tokens to secrets.json file
    const tokens = {
      clientId: CLIENT_ID,
      clientSecret: CLIENT_SECRET,
      accessToken: tokenResponse.data.access_token,
      refreshToken: tokenResponse.data.refresh_token,
    };

    fs.writeFileSync("secrets.json", JSON.stringify(tokens, null, 2));

    res.send("Authentication successful! Tokens saved to secrets.json");

    setTimeout(() => {
      server.close();
      console.log(
        "Tokens saved to secrets.json. You can now close this window."
      );
    }, 3000);
  } catch (error) {
    res.send("Error during authentication: " + error.message);
  }
});

const server = app.listen(PORT, () => {
  console.log(
    `Please open http://localhost:${PORT}/login in your browser to authenticate`
  );
});

```

--------------------------------------------------------------------------------
/mcp/spotify-mcp.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fetch from "node-fetch";

const server = new McpServer({
  name: "SpotifyServer",
  version: "1.0.0",
  capabilities: {
    tools: {},
  },
});

let spotifyAuthInfo = {
  accessToken: "",
  refreshToken: "",
  clientId: "",
  clientSecret: "",
};

// Refresh token when needed
async function getValidAccessToken() {
  if (!spotifyAuthInfo.accessToken || !spotifyAuthInfo.refreshToken) {
    throw new Error(
      "No access token available. Please set credentials first using the set-spotify-credentials tool."
    );
  }

  try {
    // Try using current token
    const response = await fetch("https://api.spotify.com/v1/me", {
      headers: {
        Authorization: `Bearer ${spotifyAuthInfo.accessToken}`,
      },
    });

    // If token works, return it
    if (response.ok) {
      return spotifyAuthInfo.accessToken;
    }

    console.error("Access token expired, refreshing...");

    // If token doesn't work, refresh it
    const refreshResponse = await fetch(
      "https://accounts.spotify.com/api/token",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          Authorization:
            "Basic " +
            Buffer.from(
              spotifyAuthInfo.clientId + ":" + spotifyAuthInfo.clientSecret
            ).toString("base64"),
        },
        body: new URLSearchParams({
          grant_type: "refresh_token",
          refresh_token: spotifyAuthInfo.refreshToken,
        }),
      }
    );

    const data = (await refreshResponse.json()) as any;

    if (data.access_token) {
      console.error("Successfully refreshed access token");
      spotifyAuthInfo.accessToken = data.access_token;
      return spotifyAuthInfo.accessToken;
    }

    throw new Error("Failed to refresh access token");
  } catch (error) {
    throw new Error(
      "Error with access token: " +
        (error instanceof Error ? error.message : String(error))
    );
  }
}

// Set credentials tool
server.tool(
  "set-spotify-credentials",
  {
    clientId: z.string().describe("The Spotify Client ID"),
    clientSecret: z.string().describe("The Spotify Client Secret"),
    accessToken: z.string().describe("The Spotify Access Token"),
    refreshToken: z.string().describe("The Spotify Refresh Token"),
  },
  async ({ clientId, clientSecret, accessToken, refreshToken }) => {
    spotifyAuthInfo.clientId = clientId;
    spotifyAuthInfo.clientSecret = clientSecret;
    spotifyAuthInfo.accessToken = accessToken;
    spotifyAuthInfo.refreshToken = refreshToken;

    return {
      content: [
        {
          type: "text",
          text: "Spotify credentials set successfully. You can now use other Spotify tools.",
        },
      ],
    };
  }
);

// Check credentials tool
server.tool("check-credentials-status", {}, async () => {
  if (
    !spotifyAuthInfo.accessToken ||
    !spotifyAuthInfo.refreshToken ||
    !spotifyAuthInfo.clientId ||
    !spotifyAuthInfo.clientSecret
  ) {
    return {
      content: [
        {
          type: "text",
          text: "Spotify credentials are not set. Please use the set-spotify-credentials tool.",
        },
      ],
    };
  }

  try {
    const accessToken = await getValidAccessToken();

    const response = await fetch("https://api.spotify.com/v1/me", {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    if (response.ok) {
      const userData = (await response.json()) as any;
      return {
        content: [
          {
            type: "text",
            text: `Spotify credentials are valid.\nLogged in as: ${
              userData.display_name
            } (${userData.email || "email not available"})`,
          },
        ],
      };
    } else {
      return {
        content: [
          {
            type: "text",
            text: `Spotify credentials may be invalid. Status code: ${response.status}`,
          },
        ],
        isError: true,
      };
    }
  } catch (error) {
    return {
      content: [
        {
          type: "text",
          text: `Error checking credentials: ${
            error instanceof Error ? error.message : String(error)
          }`,
        },
      ],
      isError: true,
    };
  }
});

// Search tracks
server.tool(
  "search-tracks",
  {
    query: z.string().describe("Search query for tracks"),
    limit: z
      .number()
      .min(1)
      .max(50)
      .default(10)
      .describe("Number of results to return"),
  },
  async ({ query, limit }) => {
    try {
      const accessToken = await getValidAccessToken();

      const response = await fetch(
        `https://api.spotify.com/v1/search?q=${encodeURIComponent(
          query
        )}&type=track&limit=${limit}`,
        {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        }
      );

      const data = (await response.json()) as any;

      if (!response.ok) {
        return {
          content: [
            {
              type: "text",
              text: `Error searching tracks: ${JSON.stringify(data)}`,
            },
          ],
          isError: true,
        };
      }

      const tracks = data.tracks.items.map((track: any) => ({
        id: track.id,
        name: track.name,
        artist: track.artists.map((artist: any) => artist.name).join(", "),
        album: track.album.name,
        uri: track.uri,
      }));

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(tracks, null, 2),
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Failed to search tracks: ${
              error instanceof Error ? error.message : String(error)
            }`,
          },
        ],
        isError: true,
      };
    }
  }
);

// Get current user
server.tool("get-current-user", {}, async () => {
  try {
    const accessToken = await getValidAccessToken();

    const response = await fetch("https://api.spotify.com/v1/me", {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    const data = (await response.json()) as any;

    if (!response.ok) {
      return {
        content: [
          {
            type: "text",
            text: `Error getting user profile: ${JSON.stringify(data)}`,
          },
        ],
        isError: true,
      };
    }

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(
            {
              id: data.id,
              name: data.display_name,
              email: data.email,
              country: data.country,
            },
            null,
            2
          ),
        },
      ],
    };
  } catch (error) {
    return {
      content: [
        {
          type: "text",
          text: `Failed to get user profile: ${
            error instanceof Error ? error.message : String(error)
          }`,
        },
      ],
      isError: true,
    };
  }
});

// Create playlist
server.tool(
  "create-playlist",
  {
    name: z.string().describe("Name of the playlist"),
    description: z.string().optional().describe("Description of the playlist"),
  },
  async ({ name, description = "" }) => {
    try {
      const accessToken = await getValidAccessToken();

      // Get user ID
      const userResponse = await fetch("https://api.spotify.com/v1/me", {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      });

      const userData = (await userResponse.json()) as any;
      const userId = userData.id;

      // Create playlist
      const response = await fetch(
        `https://api.spotify.com/v1/users/${userId}/playlists`,
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${accessToken}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            name,
            description,
            public: false,
          }),
        }
      );

      const data = (await response.json()) as any;

      if (!response.ok) {
        return {
          content: [
            {
              type: "text",
              text: `Error creating playlist: ${JSON.stringify(data)}`,
            },
          ],
          isError: true,
        };
      }

      return {
        content: [
          {
            type: "text",
            text: `Playlist created successfully!\nName: ${data.name}\nID: ${data.id}\nURL: ${data.external_urls.spotify}`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Failed to create playlist: ${
              error instanceof Error ? error.message : String(error)
            }`,
          },
        ],
        isError: true,
      };
    }
  }
);

// Add tracks to playlist
server.tool(
  "add-tracks-to-playlist",
  {
    playlistId: z.string().describe("The Spotify playlist ID"),
    trackUris: z
      .array(z.string())
      .describe("Array of Spotify track URIs to add"),
  },
  async ({ playlistId, trackUris }) => {
    try {
      const accessToken = await getValidAccessToken();

      const response = await fetch(
        `https://api.spotify.com/v1/playlists/${playlistId}/tracks`,
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${accessToken}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            uris: trackUris,
          }),
        }
      );

      const data = (await response.json()) as any;

      if (!response.ok) {
        return {
          content: [
            {
              type: "text",
              text: `Error adding tracks: ${JSON.stringify(data)}`,
            },
          ],
          isError: true,
        };
      }

      return {
        content: [
          {
            type: "text",
            text: `Successfully added ${trackUris.length} track(s) to playlist!`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Failed to add tracks: ${
              error instanceof Error ? error.message : String(error)
            }`,
          },
        ],
        isError: true,
      };
    }
  }
);

// Get recommendations
server.tool(
  "get-recommendations",
  {
    seedTracks: z
      .array(z.string())
      .max(5)
      .describe("Spotify track IDs to use as seeds (max 5)"),
    limit: z
      .number()
      .min(1)
      .max(100)
      .default(20)
      .describe("Number of recommendations to return"),
  },
  async ({ seedTracks, limit }) => {
    try {
      const accessToken = await getValidAccessToken();

      if (seedTracks.length === 0) {
        return {
          content: [
            {
              type: "text",
              text: "Error: At least one seed track is required",
            },
          ],
          isError: true,
        };
      }

      const response = await fetch(
        `https://api.spotify.com/v1/recommendations?seed_tracks=${seedTracks.join(
          ","
        )}&limit=${limit}`,
        {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        }
      );

      const data = (await response.json()) as any;

      if (!response.ok) {
        return {
          content: [
            {
              type: "text",
              text: `Error getting recommendations: ${JSON.stringify(data)}`,
            },
          ],
          isError: true,
        };
      }

      const tracks = data.tracks.map((track: any) => ({
        id: track.id,
        name: track.name,
        artist: track.artists.map((artist: any) => artist.name).join(", "),
        album: track.album.name,
        uri: track.uri,
      }));

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(tracks, null, 2),
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Failed to get recommendations: ${
              error instanceof Error ? error.message : String(error)
            }`,
          },
        ],
        isError: true,
      };
    }
  }
);

// Start the server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Spotify MCP Server running on stdio");
  console.error(
    "No credentials are pre-loaded. Users must set credentials with set-spotify-credentials tool."
  );
}

main().catch((error) => {
  console.error("Fatal error:", error);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/mcp/spotify-mcp-sse.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { z } from "zod";
import fetch from "node-fetch";
import express from "express";
import cors from "cors";

const app = express();
app.use(
  cors({
    origin: "*",
    methods: ["GET", "POST"],
    allowedHeaders: ["Content-Type"],
  })
);

const server = new McpServer({
  name: "SpotifyServer",
  version: "1.0.0",
  capabilities: {
    tools: {},
  },
});

const transports = {};

let spotifyAuthInfo = {
  accessToken: "",
  refreshToken: "",
  clientId: "",
  clientSecret: "",
};

// Refresh token when needed
async function getValidAccessToken() {
  if (!spotifyAuthInfo.accessToken || !spotifyAuthInfo.refreshToken) {
    throw new Error(
      "No access token available. Please set credentials first using the set-spotify-credentials tool."
    );
  }

  try {
    // Try using current token
    const response = await fetch("https://api.spotify.com/v1/me", {
      headers: {
        Authorization: `Bearer ${spotifyAuthInfo.accessToken}`,
      },
    });

    // If token works, return it
    if (response.ok) {
      return spotifyAuthInfo.accessToken;
    }

    console.error("Access token expired, refreshing...");

    // If token doesn't work, refresh it
    const refreshResponse = await fetch(
      "https://accounts.spotify.com/api/token",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          Authorization:
            "Basic " +
            Buffer.from(
              spotifyAuthInfo.clientId + ":" + spotifyAuthInfo.clientSecret
            ).toString("base64"),
        },
        body: new URLSearchParams({
          grant_type: "refresh_token",
          refresh_token: spotifyAuthInfo.refreshToken,
        }),
      }
    );

    const data = (await refreshResponse.json()) as any;

    if (data.access_token) {
      console.error("Successfully refreshed access token");
      spotifyAuthInfo.accessToken = data.access_token;
      return spotifyAuthInfo.accessToken;
    }

    throw new Error("Failed to refresh access token");
  } catch (error) {
    throw new Error(
      "Error with access token: " +
        (error instanceof Error ? error.message : String(error))
    );
  }
}

// Set credentials tool
server.tool(
  "set-spotify-credentials",
  {
    clientId: z.string().describe("The Spotify Client ID"),
    clientSecret: z.string().describe("The Spotify Client Secret"),
    accessToken: z.string().describe("The Spotify Access Token"),
    refreshToken: z.string().describe("The Spotify Refresh Token"),
  },
  async ({ clientId, clientSecret, accessToken, refreshToken }) => {
    spotifyAuthInfo.clientId = clientId;
    spotifyAuthInfo.clientSecret = clientSecret;
    spotifyAuthInfo.accessToken = accessToken;
    spotifyAuthInfo.refreshToken = refreshToken;

    return {
      content: [
        {
          type: "text",
          text: "Spotify credentials set successfully. You can now use other Spotify tools.",
        },
      ],
    };
  }
);

// Check credentials tool
server.tool("check-credentials-status", {}, async () => {
  if (
    !spotifyAuthInfo.accessToken ||
    !spotifyAuthInfo.refreshToken ||
    !spotifyAuthInfo.clientId ||
    !spotifyAuthInfo.clientSecret
  ) {
    return {
      content: [
        {
          type: "text",
          text: "Spotify credentials are not set. Please use the set-spotify-credentials tool.",
        },
      ],
    };
  }

  try {
    const accessToken = await getValidAccessToken();

    const response = await fetch("https://api.spotify.com/v1/me", {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    if (response.ok) {
      const userData = (await response.json()) as any;
      return {
        content: [
          {
            type: "text",
            text: `Spotify credentials are valid.\nLogged in as: ${
              userData.display_name
            } (${userData.email || "email not available"})`,
          },
        ],
      };
    } else {
      return {
        content: [
          {
            type: "text",
            text: `Spotify credentials may be invalid. Status code: ${response.status}`,
          },
        ],
        isError: true,
      };
    }
  } catch (error) {
    return {
      content: [
        {
          type: "text",
          text: `Error checking credentials: ${
            error instanceof Error ? error.message : String(error)
          }`,
        },
      ],
      isError: true,
    };
  }
});

// Search tracks
server.tool(
  "search-tracks",
  {
    query: z.string().describe("Search query for tracks"),
    limit: z
      .number()
      .min(1)
      .max(50)
      .default(10)
      .describe("Number of results to return"),
  },
  async ({ query, limit }) => {
    try {
      const accessToken = await getValidAccessToken();

      const response = await fetch(
        `https://api.spotify.com/v1/search?q=${encodeURIComponent(
          query
        )}&type=track&limit=${limit}`,
        {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        }
      );

      const data = (await response.json()) as any;

      if (!response.ok) {
        return {
          content: [
            {
              type: "text",
              text: `Error searching tracks: ${JSON.stringify(data)}`,
            },
          ],
          isError: true,
        };
      }

      const tracks = data.tracks.items.map((track: any) => ({
        id: track.id,
        name: track.name,
        artist: track.artists.map((artist: any) => artist.name).join(", "),
        album: track.album.name,
        uri: track.uri,
      }));

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(tracks, null, 2),
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Failed to search tracks: ${
              error instanceof Error ? error.message : String(error)
            }`,
          },
        ],
        isError: true,
      };
    }
  }
);

// Get current user
server.tool("get-current-user", {}, async () => {
  try {
    const accessToken = await getValidAccessToken();

    const response = await fetch("https://api.spotify.com/v1/me", {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    const data = (await response.json()) as any;

    if (!response.ok) {
      return {
        content: [
          {
            type: "text",
            text: `Error getting user profile: ${JSON.stringify(data)}`,
          },
        ],
        isError: true,
      };
    }

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(
            {
              id: data.id,
              name: data.display_name,
              email: data.email,
              country: data.country,
            },
            null,
            2
          ),
        },
      ],
    };
  } catch (error) {
    return {
      content: [
        {
          type: "text",
          text: `Failed to get user profile: ${
            error instanceof Error ? error.message : String(error)
          }`,
        },
      ],
      isError: true,
    };
  }
});

// Create playlist
server.tool(
  "create-playlist",
  {
    name: z.string().describe("Name of the playlist"),
    description: z.string().optional().describe("Description of the playlist"),
  },
  async ({ name, description = "" }) => {
    try {
      const accessToken = await getValidAccessToken();

      // Get user ID
      const userResponse = await fetch("https://api.spotify.com/v1/me", {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      });

      const userData = (await userResponse.json()) as any;
      const userId = userData.id;

      // Create playlist
      const response = await fetch(
        `https://api.spotify.com/v1/users/${userId}/playlists`,
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${accessToken}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            name,
            description,
            public: false,
          }),
        }
      );

      const data = (await response.json()) as any;

      if (!response.ok) {
        return {
          content: [
            {
              type: "text",
              text: `Error creating playlist: ${JSON.stringify(data)}`,
            },
          ],
          isError: true,
        };
      }

      return {
        content: [
          {
            type: "text",
            text: `Playlist created successfully!\nName: ${data.name}\nID: ${data.id}\nURL: ${data.external_urls.spotify}`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Failed to create playlist: ${
              error instanceof Error ? error.message : String(error)
            }`,
          },
        ],
        isError: true,
      };
    }
  }
);

// Add tracks to playlist
server.tool(
  "add-tracks-to-playlist",
  {
    playlistId: z.string().describe("The Spotify playlist ID"),
    trackUris: z
      .array(z.string())
      .describe("Array of Spotify track URIs to add"),
  },
  async ({ playlistId, trackUris }) => {
    try {
      const accessToken = await getValidAccessToken();

      const response = await fetch(
        `https://api.spotify.com/v1/playlists/${playlistId}/tracks`,
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${accessToken}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            uris: trackUris,
          }),
        }
      );

      const data = (await response.json()) as any;

      if (!response.ok) {
        return {
          content: [
            {
              type: "text",
              text: `Error adding tracks: ${JSON.stringify(data)}`,
            },
          ],
          isError: true,
        };
      }

      return {
        content: [
          {
            type: "text",
            text: `Successfully added ${trackUris.length} track(s) to playlist!`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Failed to add tracks: ${
              error instanceof Error ? error.message : String(error)
            }`,
          },
        ],
        isError: true,
      };
    }
  }
);

// Get recommendations
server.tool(
  "get-recommendations",
  {
    seedTracks: z
      .array(z.string())
      .max(5)
      .describe("Spotify track IDs to use as seeds (max 5)"),
    limit: z
      .number()
      .min(1)
      .max(100)
      .default(20)
      .describe("Number of recommendations to return"),
  },
  async ({ seedTracks, limit }) => {
    try {
      const accessToken = await getValidAccessToken();

      if (seedTracks.length === 0) {
        return {
          content: [
            {
              type: "text",
              text: "Error: At least one seed track is required",
            },
          ],
          isError: true,
        };
      }

      const response = await fetch(
        `https://api.spotify.com/v1/recommendations?seed_tracks=${seedTracks.join(
          ","
        )}&limit=${limit}`,
        {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        }
      );

      const data = (await response.json()) as any;

      if (!response.ok) {
        return {
          content: [
            {
              type: "text",
              text: `Error getting recommendations: ${JSON.stringify(data)}`,
            },
          ],
          isError: true,
        };
      }

      const tracks = data.tracks.map((track: any) => ({
        id: track.id,
        name: track.name,
        artist: track.artists.map((artist: any) => artist.name).join(", "),
        album: track.album.name,
        uri: track.uri,
      }));

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(tracks, null, 2),
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Failed to get recommendations: ${
              error instanceof Error ? error.message : String(error)
            }`,
          },
        ],
        isError: true,
      };
    }
  }
);

let transport: SSEServerTransport | null = null;

app.get("/sse", (req, res) => {
  transport = new SSEServerTransport("/messages", res);
  server.connect(transport);
});

app.post("/messages", (req, res) => {
  if (transport) {
    transport.handlePostMessage(req, res);
  } else {
    res.status(503).send("No active transport");
  }
});

app.listen(3001, () => {
  console.log("Listening on port 3001");
});

```

--------------------------------------------------------------------------------
/mcp/spotify-mcp-http.js:
--------------------------------------------------------------------------------

```javascript
import express from "express";
import cors from "cors";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
import fetch from "node-fetch";

// GLOBAL credentials storage (shared across all requests)
let globalSpotifyAuthInfo = {
  accessToken: "",
  refreshToken: "",
  clientId: "",
  clientSecret: "",
};

// Helper function to create server instance
function createSpotifyMcpServer() {
  const server = new McpServer({
    name: "SpotifyServer",
    version: "1.0.0",
    capabilities: {
      tools: {},
    },
  });

  // Refresh token when needed
  async function getValidAccessToken() {
    if (
      !globalSpotifyAuthInfo.accessToken ||
      !globalSpotifyAuthInfo.refreshToken
    ) {
      throw new Error(
        "No access token available. Please set credentials first using the set-spotify-credentials tool."
      );
    }

    try {
      // Try using current token
      const response = await fetch("https://api.spotify.com/v1/me", {
        headers: {
          Authorization: `Bearer ${globalSpotifyAuthInfo.accessToken}`,
        },
      });

      // If token works, return it
      if (response.ok) {
        return globalSpotifyAuthInfo.accessToken;
      }

      console.log("Access token expired, refreshing...");

      // If token doesn't work, refresh it
      const refreshResponse = await fetch(
        "https://accounts.spotify.com/api/token",
        {
          method: "POST",
          headers: {
            "Content-Type": "application/x-www-form-urlencoded",
            Authorization:
              "Basic " +
              Buffer.from(
                globalSpotifyAuthInfo.clientId +
                  ":" +
                  globalSpotifyAuthInfo.clientSecret
              ).toString("base64"),
          },
          body: new URLSearchParams({
            grant_type: "refresh_token",
            refresh_token: globalSpotifyAuthInfo.refreshToken,
          }),
        }
      );

      const data = await refreshResponse.json();

      if (data.access_token) {
        console.log("Successfully refreshed access token");
        globalSpotifyAuthInfo.accessToken = data.access_token;
        return globalSpotifyAuthInfo.accessToken;
      }

      throw new Error("Failed to refresh access token");
    } catch (error) {
      throw new Error("Error with access token: " + error.message);
    }
  }

  // Set credentials tool
  server.tool(
    "set-spotify-credentials",
    {
      clientId: z.string().describe("The Spotify Client ID"),
      clientSecret: z.string().describe("The Spotify Client Secret"),
      accessToken: z.string().describe("The Spotify Access Token"),
      refreshToken: z.string().describe("The Spotify Refresh Token"),
    },
    async ({ clientId, clientSecret, accessToken, refreshToken }) => {
      globalSpotifyAuthInfo.clientId = clientId;
      globalSpotifyAuthInfo.clientSecret = clientSecret;
      globalSpotifyAuthInfo.accessToken = accessToken;
      globalSpotifyAuthInfo.refreshToken = refreshToken;

      return {
        content: [
          {
            type: "text",
            text: "Spotify credentials set successfully. You can now use other Spotify tools.",
          },
        ],
      };
    }
  );

  // Check credentials tool
  server.tool("check-credentials-status", {}, async () => {
    if (
      !globalSpotifyAuthInfo.accessToken ||
      !globalSpotifyAuthInfo.refreshToken ||
      !globalSpotifyAuthInfo.clientId ||
      !globalSpotifyAuthInfo.clientSecret
    ) {
      return {
        content: [
          {
            type: "text",
            text: "Spotify credentials are not set. Please use the set-spotify-credentials tool.",
          },
        ],
      };
    }

    try {
      const accessToken = await getValidAccessToken();

      const response = await fetch("https://api.spotify.com/v1/me", {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      });

      if (response.ok) {
        const userData = await response.json();
        return {
          content: [
            {
              type: "text",
              text: `Spotify credentials are valid.\nLogged in as: ${
                userData.display_name
              } (${userData.email || "email not available"})`,
            },
          ],
        };
      } else {
        return {
          content: [
            {
              type: "text",
              text: `Spotify credentials may be invalid. Status code: ${response.status}`,
            },
          ],
          isError: true,
        };
      }
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Error checking credentials: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  });

  // Search tracks
  server.tool(
    "search-tracks",
    {
      query: z.string().describe("Search query for tracks"),
      limit: z
        .number()
        .min(1)
        .max(50)
        .default(10)
        .describe("Number of results to return"),
    },
    async ({ query, limit }) => {
      try {
        const accessToken = await getValidAccessToken();

        const response = await fetch(
          `https://api.spotify.com/v1/search?q=${encodeURIComponent(
            query
          )}&type=track&limit=${limit}`,
          {
            headers: {
              Authorization: `Bearer ${accessToken}`,
            },
          }
        );

        const data = await response.json();

        if (!response.ok) {
          return {
            content: [
              {
                type: "text",
                text: `Error searching tracks: ${JSON.stringify(data)}`,
              },
            ],
            isError: true,
          };
        }

        const tracks = data.tracks.items.map((track) => ({
          id: track.id,
          name: track.name,
          artist: track.artists.map((artist) => artist.name).join(", "),
          album: track.album.name,
          uri: track.uri,
        }));

        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(tracks, null, 2),
            },
          ],
        };
      } catch (error) {
        return {
          content: [
            {
              type: "text",
              text: `Failed to search tracks: ${error.message}`,
            },
          ],
          isError: true,
        };
      }
    }
  );

  // Get current user
  server.tool("get-current-user", {}, async () => {
    try {
      const accessToken = await getValidAccessToken();

      const response = await fetch("https://api.spotify.com/v1/me", {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      });

      const data = await response.json();

      if (!response.ok) {
        return {
          content: [
            {
              type: "text",
              text: `Error getting user profile: ${JSON.stringify(data)}`,
            },
          ],
          isError: true,
        };
      }

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(
              {
                id: data.id,
                name: data.display_name,
                email: data.email,
                country: data.country,
              },
              null,
              2
            ),
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Failed to get user profile: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  });

  // Create playlist
  server.tool(
    "create-playlist",
    {
      name: z.string().describe("Name of the playlist"),
      description: z
        .string()
        .optional()
        .describe("Description of the playlist"),
    },
    async ({ name, description = "" }) => {
      try {
        const accessToken = await getValidAccessToken();

        // Get user ID
        const userResponse = await fetch("https://api.spotify.com/v1/me", {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        });

        const userData = await userResponse.json();
        const userId = userData.id;

        // Create playlist
        const response = await fetch(
          `https://api.spotify.com/v1/users/${userId}/playlists`,
          {
            method: "POST",
            headers: {
              Authorization: `Bearer ${accessToken}`,
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              name,
              description,
              public: false,
            }),
          }
        );

        const data = await response.json();

        if (!response.ok) {
          return {
            content: [
              {
                type: "text",
                text: `Error creating playlist: ${JSON.stringify(data)}`,
              },
            ],
            isError: true,
          };
        }

        return {
          content: [
            {
              type: "text",
              text: `Playlist created successfully!\nName: ${data.name}\nID: ${data.id}\nURL: ${data.external_urls.spotify}`,
            },
          ],
        };
      } catch (error) {
        return {
          content: [
            {
              type: "text",
              text: `Failed to create playlist: ${error.message}`,
            },
          ],
          isError: true,
        };
      }
    }
  );

  // Add tracks to playlist
  server.tool(
    "add-tracks-to-playlist",
    {
      playlistId: z.string().describe("The Spotify playlist ID"),
      trackUris: z
        .array(z.string())
        .describe("Array of Spotify track URIs to add"),
    },
    async ({ playlistId, trackUris }) => {
      try {
        const accessToken = await getValidAccessToken();

        const response = await fetch(
          `https://api.spotify.com/v1/playlists/${playlistId}/tracks`,
          {
            method: "POST",
            headers: {
              Authorization: `Bearer ${accessToken}`,
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              uris: trackUris,
            }),
          }
        );

        const data = await response.json();

        if (!response.ok) {
          return {
            content: [
              {
                type: "text",
                text: `Error adding tracks: ${JSON.stringify(data)}`,
              },
            ],
            isError: true,
          };
        }

        return {
          content: [
            {
              type: "text",
              text: `Successfully added ${trackUris.length} track(s) to playlist!`,
            },
          ],
        };
      } catch (error) {
        return {
          content: [
            {
              type: "text",
              text: `Failed to add tracks: ${error.message}`,
            },
          ],
          isError: true,
        };
      }
    }
  );

  // Get recommendations
  server.tool(
    "get-recommendations",
    {
      seedTracks: z
        .array(z.string())
        .max(5)
        .describe("Spotify track IDs to use as seeds (max 5)"),
      limit: z
        .number()
        .min(1)
        .max(100)
        .default(20)
        .describe("Number of recommendations to return"),
    },
    async ({ seedTracks, limit }) => {
      try {
        const accessToken = await getValidAccessToken();

        if (seedTracks.length === 0) {
          return {
            content: [
              {
                type: "text",
                text: "Error: At least one seed track is required",
              },
            ],
            isError: true,
          };
        }

        const response = await fetch(
          `https://api.spotify.com/v1/recommendations?seed_tracks=${seedTracks.join(
            ","
          )}&limit=${limit}`,
          {
            headers: {
              Authorization: `Bearer ${accessToken}`,
            },
          }
        );

        const data = await response.json();

        if (!response.ok) {
          return {
            content: [
              {
                type: "text",
                text: `Error getting recommendations: ${JSON.stringify(data)}`,
              },
            ],
            isError: true,
          };
        }

        const tracks = data.tracks.map((track) => ({
          id: track.id,
          name: track.name,
          artist: track.artists.map((artist) => artist.name).join(", "),
          album: track.album.name,
          uri: track.uri,
        }));

        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(tracks, null, 2),
            },
          ],
        };
      } catch (error) {
        return {
          content: [
            {
              type: "text",
              text: `Failed to get recommendations: ${error.message}`,
            },
          ],
          isError: true,
        };
      }
    }
  );

  return server;
}

// Create Express app
const app = express();
app.use(express.json());

// CORS configuration
app.use(
  cors({
    origin: "*", // Configure this for production
    exposedHeaders: ["Mcp-Session-Id"],
    allowedHeaders: ["Content-Type", "mcp-session-id"],
  })
);

// Health check endpoint
app.get("/health", (req, res) => {
  res.json({ status: "healthy", timestamp: new Date().toISOString() });
});

// MCP endpoint (stateless mode)
app.post("/mcp", async (req, res) => {
  try {
    // Create new server instance for each request (stateless)
    const server = createSpotifyMcpServer();
    const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: undefined, // No session management
    });

    // Clean up when request closes
    res.on("close", () => {
      console.log("Request closed, cleaning up");
      transport.close();
      server.close();
    });

    // Connect server to transport
    await server.connect(transport);

    // Handle the request
    await transport.handleRequest(req, res, req.body);
  } catch (error) {
    console.error("Error handling MCP request:", error);
    if (!res.headersSent) {
      res.status(500).json({
        jsonrpc: "2.0",
        error: {
          code: -32603,
          message: "Internal server error",
        },
        id: null,
      });
    }
  }
});

// Start the server
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
  console.log(`Spotify MCP Server running on port ${PORT}`);
  console.log(`Health check: http://localhost:${PORT}/health`);
  console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);
  console.log(`Mode: STATELESS`);
});

```

--------------------------------------------------------------------------------
/mcp/spotify-mcp-oauth-http.js:
--------------------------------------------------------------------------------

```javascript
import express from "express";
import cors from "cors";
import { randomUUID } from "node:crypto";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import fetch from "node-fetch";

// Environment variables
const SPOTIFY_CLIENT_ID = process.env.SPOTIFY_CLIENT_ID;
const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET;
const SPOTIFY_REDIRECT_URI =
  process.env.SPOTIFY_REDIRECT_URI || "http://localhost:8080/callback/spotify";

if (!SPOTIFY_CLIENT_ID || !SPOTIFY_CLIENT_SECRET) {
  console.error(
    "Missing required environment variables: SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET"
  );
  process.exit(1);
}

console.log("🎵 Spotify Config:");
console.log("- Client ID:", SPOTIFY_CLIENT_ID);
console.log("- Redirect URI:", SPOTIFY_REDIRECT_URI);

// Session storage
const transports = new Map();
const sessionTokens = new Map();
const sessionActivity = new Map(); // Track last activity per session

// Session cleanup configuration
const INACTIVITY_TIMEOUT = 8 * 60 * 1000; // 8 minutes
const CLEANUP_INTERVAL = 3 * 60 * 1000; // 3 minutes

// Simple cleanup function
function cleanupSession(sessionId) {
  console.log(`🧹 Cleaning up session: ${sessionId}`);

  const transport = transports.get(sessionId);
  if (transport) {
    try {
      transport.close();
    } catch (error) {
      console.warn(`Warning closing transport:`, error.message);
    }
    transports.delete(sessionId);
  }

  sessionTokens.delete(sessionId);
  sessionActivity.delete(sessionId);
}

// Update session activity
function updateSessionActivity(sessionId) {
  sessionActivity.set(sessionId, Date.now());
}

// Cleanup inactive sessions
function cleanupInactiveSessions() {
  const now = Date.now();
  const inactiveSessions = [];

  for (const [sessionId, lastActivity] of sessionActivity.entries()) {
    if (now - lastActivity > INACTIVITY_TIMEOUT) {
      inactiveSessions.push(sessionId);
    }
  }

  if (inactiveSessions.length > 0) {
    console.log(`🧹 Cleaning up ${inactiveSessions.length} inactive sessions`);
    inactiveSessions.forEach(cleanupSession);
  }
}

// Start cleanup interval
setInterval(() => {
  try {
    cleanupInactiveSessions();
  } catch (error) {
    console.error('Error during inactive session cleanup:', error);
  }
}, CLEANUP_INTERVAL);

// Helper functions
function isTokenExpired(tokens) {
  if (!tokens.expiresAt) return true;
  return Date.now() >= tokens.expiresAt;
}

function getSpotifyAuthUrl(sessionId) {
  const params = new URLSearchParams({
    response_type: "code",
    client_id: SPOTIFY_CLIENT_ID,
    scope:
      "user-read-private user-read-email playlist-read-private playlist-modify-private playlist-modify-public",
    redirect_uri: SPOTIFY_REDIRECT_URI,
    state: sessionId,
  });
  return `https://accounts.spotify.com/authorize?${params.toString()}`;
}

async function exchangeCodeForTokens(code) {
  const response = await fetch("https://accounts.spotify.com/api/token", {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization:
        "Basic " +
        Buffer.from(SPOTIFY_CLIENT_ID + ":" + SPOTIFY_CLIENT_SECRET).toString(
          "base64"
        ),
    },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code: code,
      redirect_uri: SPOTIFY_REDIRECT_URI,
    }),
  });

  const data = await response.json();

  if (!response.ok) {
    throw new Error(
      `Token exchange failed: ${data.error_description || data.error}`
    );
  }

  return {
    accessToken: data.access_token,
    refreshToken: data.refresh_token,
    expiresAt: Date.now() + data.expires_in * 1000,
  };
}

async function refreshSpotifyToken(tokens) {
  const response = await fetch("https://accounts.spotify.com/api/token", {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization:
        "Basic " +
        Buffer.from(SPOTIFY_CLIENT_ID + ":" + SPOTIFY_CLIENT_SECRET).toString(
          "base64"
        ),
    },
    body: new URLSearchParams({
      grant_type: "refresh_token",
      refresh_token: tokens.refreshToken,
    }),
  });

  const data = await response.json();

  if (!response.ok) {
    throw new Error(
      `Token refresh failed: ${data.error_description || data.error}`
    );
  }

  return {
    accessToken: data.access_token,
    refreshToken: data.refresh_token || tokens.refreshToken,
    expiresAt: Date.now() + data.expires_in * 1000,
  };
}

async function getValidAccessToken(sessionId) {
  let tokens = sessionTokens.get(sessionId);

  if (!tokens) {
    return null;
  }

  if (isTokenExpired(tokens)) {
    try {
      tokens = await refreshSpotifyToken(tokens);
      sessionTokens.set(sessionId, tokens);
    } catch (error) {
      console.log(
        `❌ Token refresh failed for session ${sessionId}:`,
        error.message
      );
      sessionTokens.delete(sessionId);
      return null;
    }
  }

  return tokens.accessToken;
}

async function handleSpotifyTool(sessionId, apiCall) {
  const accessToken = await getValidAccessToken(sessionId);

  if (!accessToken) {
    console.log(`🔐 Auth required for session: ${sessionId}`);
    const baseUrl =
      process.env.SPOTIFY_REDIRECT_URI?.replace("/callback/spotify", "") ||
      "http://localhost:8080";
    const authUrl = `${baseUrl}/auth?session=${sessionId}`;

    return {
      content: [
        {
          type: "text",
          text: `🎵 **Spotify Authentication Required**

To use Spotify features, please visit:

${authUrl}

This will redirect you to connect your Spotify account. After authentication, return here and try your request again.`,
        },
      ],
    };
  }

  try {
    console.log(`✅ Executing Spotify API call for session: ${sessionId}`);
    return await apiCall(accessToken);
  } catch (error) {
    if (error.response?.status === 401) {
      console.log(`❌ Token expired for session: ${sessionId}`);
      sessionTokens.delete(sessionId);
      const baseUrl =
        process.env.SPOTIFY_REDIRECT_URI?.replace("/callback/spotify", "") ||
        "http://localhost:8080";
      const authUrl = `${baseUrl}/auth?session=${sessionId}`;
      return {
        content: [
          {
            type: "text",
            text: `🔐 **Spotify Authentication Expired**

Your Spotify session has expired. Please visit:

${authUrl}

After completing authentication, return here and try your request again.`,
          },
        ],
        isError: true,
      };
    }

    console.log(
      `❌ Spotify API error for session ${sessionId}:`,
      error.message
    );
    return {
      content: [
        {
          type: "text",
          text: `❌ **Spotify API Error**

${error.message}`,
        },
      ],
      isError: true,
    };
  }
}

// Create MCP server instance with session context
function createSpotifyMcpServer(sessionId) {
  const server = new McpServer({
    name: "SpotifyServer",
    version: "1.0.0",
  });

  // Search tracks
  server.registerTool(
    "search-tracks",
    {
      title: "Search Spotify Tracks",
      description: "Search for tracks on Spotify",
      inputSchema: {
        query: z.string().describe("Search query for tracks"),
        limit: z
          .number()
          .min(1)
          .max(50)
          .default(10)
          .describe("Number of results to return"),
      },
    },
    async ({ query, limit }) => {
      return await handleSpotifyTool(sessionId, async (accessToken) => {
        const response = await fetch(
          `https://api.spotify.com/v1/search?q=${encodeURIComponent(
            query
          )}&type=track&limit=${limit}`,
          {
            headers: {
              Authorization: `Bearer ${accessToken}`,
            },
          }
        );

        const data = await response.json();

        if (!response.ok) {
          throw new Error(
            `Spotify API error: ${data.error?.message || "Unknown error"}`
          );
        }

        const tracks = data.tracks.items.map((track) => ({
          id: track.id,
          name: track.name,
          artist: track.artists.map((artist) => artist.name).join(", "),
          album: track.album.name,
          uri: track.uri,
        }));

        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(tracks, null, 2),
            },
          ],
        };
      });
    }
  );

  // Get current user
  server.registerTool(
    "get-current-user",
    {
      title: "Get Current User",
      description: "Get current Spotify user information",
    },
    async () => {
      return await handleSpotifyTool(sessionId, async (accessToken) => {
        const response = await fetch("https://api.spotify.com/v1/me", {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        });

        const data = await response.json();

        if (!response.ok) {
          throw new Error(
            `Spotify API error: ${data.error?.message || "Unknown error"}`
          );
        }

        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(
                {
                  id: data.id,
                  name: data.display_name,
                  email: data.email,
                  country: data.country,
                  followers: data.followers?.total || 0,
                },
                null,
                2
              ),
            },
          ],
        };
      });
    }
  );

  // Create playlist
  server.registerTool(
    "create-playlist",
    {
      title: "Create Playlist",
      description: "Create a new playlist on Spotify",
      inputSchema: {
        name: z.string().describe("Name of the playlist"),
        description: z
          .string()
          .optional()
          .describe("Description of the playlist"),
        public: z
          .boolean()
          .default(false)
          .describe("Whether playlist should be public"),
      },
    },
    async ({ name, description = "", public: isPublic }) => {
      return await handleSpotifyTool(sessionId, async (accessToken) => {
        // Get user ID first
        const userResponse = await fetch("https://api.spotify.com/v1/me", {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        });

        const userData = await userResponse.json();
        const userId = userData.id;

        // Create playlist
        const response = await fetch(
          `https://api.spotify.com/v1/users/${userId}/playlists`,
          {
            method: "POST",
            headers: {
              Authorization: `Bearer ${accessToken}`,
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              name,
              description,
              public: isPublic,
            }),
          }
        );

        const data = await response.json();

        if (!response.ok) {
          throw new Error(
            `Spotify API error: ${data.error?.message || "Unknown error"}`
          );
        }

        return {
          content: [
            {
              type: "text",
              text: `✅ Playlist created successfully!\n\n**${data.name}**\nID: ${data.id}\n🔗 [Open in Spotify](${data.external_urls.spotify})`,
            },
          ],
        };
      });
    }
  );

  // Add tracks to playlist
  server.registerTool(
    "add-tracks-to-playlist",
    {
      title: "Add Tracks to Playlist",
      description: "Add tracks to an existing playlist",
      inputSchema: {
        playlistId: z.string().describe("The Spotify playlist ID"),
        trackUris: z
          .array(z.string())
          .describe("Array of Spotify track URIs to add"),
      },
    },
    async ({ playlistId, trackUris }) => {
      return await handleSpotifyTool(sessionId, async (accessToken) => {
        const response = await fetch(
          `https://api.spotify.com/v1/playlists/${playlistId}/tracks`,
          {
            method: "POST",
            headers: {
              Authorization: `Bearer ${accessToken}`,
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              uris: trackUris,
            }),
          }
        );

        const data = await response.json();

        if (!response.ok) {
          throw new Error(
            `Spotify API error: ${data.error?.message || "Unknown error"}`
          );
        }

        return {
          content: [
            {
              type: "text",
              text: `✅ Successfully added ${trackUris.length} track(s) to playlist!`,
            },
          ],
        };
      });
    }
  );

  // Get recommendations
  server.registerTool(
    "get-recommendations",
    {
      title: "Get Recommendations",
      description: "Get music recommendations based on seed tracks",
      inputSchema: {
        seedTracks: z
          .array(z.string())
          .max(5)
          .describe("Spotify track IDs to use as seeds (max 5)"),
        limit: z
          .number()
          .min(1)
          .max(100)
          .default(20)
          .describe("Number of recommendations to return"),
      },
    },
    async ({ seedTracks, limit }) => {
      return await handleSpotifyTool(sessionId, async (accessToken) => {
        if (seedTracks.length === 0) {
          throw new Error("At least one seed track is required");
        }

        const response = await fetch(
          `https://api.spotify.com/v1/recommendations?seed_tracks=${seedTracks.join(
            ","
          )}&limit=${limit}`,
          {
            headers: {
              Authorization: `Bearer ${accessToken}`,
            },
          }
        );

        const data = await response.json();

        if (!response.ok) {
          throw new Error(
            `Spotify API error: ${data.error?.message || "Unknown error"}`
          );
        }

        const tracks = data.tracks.map((track) => ({
          id: track.id,
          name: track.name,
          artist: track.artists.map((artist) => artist.name).join(", "),
          album: track.album.name,
          uri: track.uri,
        }));

        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(tracks, null, 2),
            },
          ],
        };
      });
    }
  );

  return server;
}

// Express app setup
const app = express();
app.use(express.json());

// CORS configuration
app.use(
  cors({
    origin: "*",
    exposedHeaders: ["Mcp-Session-Id"],
    allowedHeaders: ["Content-Type", "mcp-session-id"],
  })
);

// Health check
app.get("/health", (req, res) => {
  res.json({
    status: "healthy",
    timestamp: new Date().toISOString(),
    activeSessions: transports.size,
  });
});

// Simple auth landing page
app.get("/auth", (req, res) => {
  const sessionId = req.query.session;

  if (!sessionId) {
    res.status(400).send("Missing session ID");
    return;
  }

  const authUrl = getSpotifyAuthUrl(sessionId);

  res.send(`
    <html>
      <head>
        <title>Connect Spotify to Claude</title>
        <style>
          body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            text-align: center;
            padding: 50px;
            background: linear-gradient(135deg, #667eea, #764ba2);
            color: white;
            margin: 0;
          }
          .container {
            background: rgba(0,0,0,0.1);
            padding: 40px;
            border-radius: 20px;
            backdrop-filter: blur(10px);
            display: inline-block;
            max-width: 500px;
          }
          .spotify-icon { font-size: 60px; margin-bottom: 20px; }
          h1 { margin: 20px 0; font-size: 28px; }
          p { font-size: 18px; line-height: 1.5; opacity: 0.9; }
          .auth-button {
            display: inline-block;
            background: #1db954;
            color: white;
            padding: 15px 30px;
            border-radius: 50px;
            text-decoration: none;
            font-size: 18px;
            font-weight: bold;
            margin: 20px 0;
            transition: all 0.3s ease;
          }
          .auth-button:hover {
            background: #1ed760;
            transform: translateY(-2px);
          }
        </style>
      </head>
      <body>
        <div class="container">
          <div class="spotify-icon">🎵</div>
          <h1>Connect Spotify to Claude</h1>
          <p>Click the button below to connect your Spotify account and enable music features in Claude.</p>

          <a href="${authUrl}" class="auth-button">Connect Spotify Account</a>

          <p style="font-size: 14px; margin-top: 30px;">
            After connecting, return to Claude to use Spotify features.
          </p>
        </div>
      </body>
    </html>
  `);
});

// Spotify OAuth callback
app.get("/callback/spotify", async (req, res) => {
  const { code, state: sessionId, error } = req.query;

  if (error) {
    console.log(`❌ OAuth error: ${error}`);
    res.status(400).send(`Authentication error: ${error}`);
    return;
  }

  if (!code || !sessionId) {
    console.log(`❌ OAuth callback missing code or sessionId`);
    res.status(400).send("Missing authorization code or session ID");
    return;
  }

  console.log(`🔄 Processing OAuth callback for session: ${sessionId}`);

  try {
    const tokens = await exchangeCodeForTokens(code);
    sessionTokens.set(sessionId, tokens);
    console.log(`✅ OAuth successful for session: ${sessionId}`);

    res.send(`
      <html>
        <head>
          <title>Spotify Connected Successfully</title>
          <style>
            body {
              font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
              text-align: center;
              padding: 50px;
              background: linear-gradient(135deg, #1db954, #1ed760);
              color: white;
              margin: 0;
            }
            .container {
              background: rgba(0,0,0,0.1);
              padding: 40px;
              border-radius: 20px;
              backdrop-filter: blur(10px);
              display: inline-block;
              max-width: 500px;
            }
            .success-icon { font-size: 60px; margin-bottom: 20px; }
            h1 { margin: 20px 0; font-size: 28px; }
            p { font-size: 18px; line-height: 1.5; opacity: 0.9; }
            .instruction {
              background: rgba(255,255,255,0.1);
              padding: 20px;
              border-radius: 10px;
              margin-top: 30px;
              border: 1px solid rgba(255,255,255,0.2);
            }
          </style>
        </head>
        <body>
          <div class="container">
            <div class="success-icon">🎵</div>
            <h1>Successfully Connected to Spotify!</h1>
            <p>Your Spotify account is now linked to Claude.</p>

            <div class="instruction">
              <strong>Next Steps:</strong><br>
              1. Return to your Claude conversation<br>
              2. Try your Spotify request again<br>
              3. This window can be closed
            </div>
          </div>
        </body>
      </html>
    `);
  } catch (error) {
    console.error(
      `❌ Token exchange failed for session ${sessionId}:`,
      error.message
    );
    res.status(500).send(`Authentication error: ${error.message}`);
  }
});

// MCP endpoint with session management
app.post("/mcp", async (req, res) => {
  try {
    const sessionId = req.headers["mcp-session-id"];
    let transport;

    if (sessionId && transports.has(sessionId)) {
      // Reuse existing transport and update activity
      transport = transports.get(sessionId);
      updateSessionActivity(sessionId);
    } else if (!sessionId && isInitializeRequest(req.body)) {
      // New initialization request
      const newSessionId = randomUUID();

      transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: () => newSessionId,
        onsessioninitialized: (sessionId) => {
          console.log(`🎯 New MCP session initialized: ${sessionId}`);
          updateSessionActivity(sessionId); // Track initial activity
        },
      });

      // Clean up transport when closed
      transport.onclose = () => {
        if (transport.sessionId) {
          console.log(`🔌 MCP session closed: ${transport.sessionId}`);
          cleanupSession(transport.sessionId);
        }
      };

      // Create and connect server with sessionId
      const server = createSpotifyMcpServer(newSessionId);
      await server.connect(transport);

      // Store transport
      transports.set(newSessionId, transport);
    } else {
      // Invalid request
      res.status(400).json({
        jsonrpc: "2.0",
        error: {
          code: -32000,
          message: "Bad Request: No valid session ID provided",
        },
        id: null,
      });
      return;
    }

    // Handle the request
    await transport.handleRequest(req, res, req.body);
  } catch (error) {
    console.error(`❌ MCP request error:`, error.message);
    if (!res.headersSent) {
      res.status(500).json({
        jsonrpc: "2.0",
        error: {
          code: -32603,
          message: "Internal server error",
        },
        id: null,
      });
    }
  }
});

// Handle GET requests for server-to-client notifications via SSE
app.get("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"];
  if (!sessionId || !transports.has(sessionId)) {
    res.status(400).send("Invalid or missing session ID");
    return;
  }

  const transport = transports.get(sessionId);
  updateSessionActivity(sessionId); // Update activity for SSE requests too
  await transport.handleRequest(req, res);
});

// Handle DELETE requests for session termination
app.delete("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"];
  if (!sessionId || !transports.has(sessionId)) {
    res.status(400).send("Invalid or missing session ID");
    return;
  }

  const transport = transports.get(sessionId);
  await transport.handleRequest(req, res);

  // Clean up on explicit delete
  cleanupSession(sessionId);
});

// Start server
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
  console.log(`🚀 Spotify OAuth MCP Server running on port ${PORT}`);
  console.log(`🔗 Health check: http://localhost:${PORT}/health`);
  console.log(`🔗 MCP endpoint: http://localhost:${PORT}/mcp`);
  console.log(`🔗 OAuth callback: http://localhost:${PORT}/callback/spotify`);
  console.log(`📊 Mode: STATEFUL with OAuth`);
  console.log(
    `⏰ Session cleanup: ${INACTIVITY_TIMEOUT / 60000} min inactivity timeout`
  );
});

```