Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
node_modules/
dist/
*.p8
.env
.env*
tokens.json
.mcp.json
.DS_Store
.vercel
73 changes: 61 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,70 @@ An MCP (Model Context Protocol) server that integrates with Apple Music, allowin
- **View** recently played tracks
- **Get** personalized recommendations

## Prerequisites
## Quick Start (Hosted)

The fastest way to get started — no Apple Developer account, no local setup, no environment variables. Just authorize and go.

### 1. Get your Music User Token

Visit **[applemusicmcp.gradientworks.ca/auth](https://applemusicmcp.gradientworks.ca/auth)** and click "Authorize with Apple Music". Sign in with your Apple ID and copy the token shown on the page.

### 2. Add to Claude Desktop

Add this to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows):

```json
{
"mcpServers": {
"apple-music-remote": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://applemusicmcp.gradientworks.ca/mcp",
"--header",
"Music-User-Token: YOUR_TOKEN_HERE"
]
}
}
}
```

Replace `YOUR_TOKEN_HERE` with the token you copied. Restart Claude Desktop.

### 3. Add to Claude Code

```bash
claude mcp add apple-music-remote \
-- npx -y mcp-remote https://applemusicmcp.gradientworks.ca/mcp \
--header "Music-User-Token: YOUR_TOKEN_HERE"
```

Your Music User Token is valid for approximately **6 months**. When it expires, revisit the auth page to get a new one.

---

## Self-Hosted Setup

If you prefer to run the server yourself, follow the instructions below.

### Prerequisites

- Node.js 18+
- An [Apple Developer Program](https://developer.apple.com/programs/) membership
- A MusicKit identifier and private key

## Apple Developer Setup
### Apple Developer Setup

### 1. Create a MusicKit Identifier
#### 1. Create a MusicKit Identifier

1. Go to [Apple Developer > Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list)
2. Click **+** to register a new identifier
3. Select **MusicKit IDs** (or **Media IDs**)
4. Enter a description (e.g., "MCP Server") and an identifier (e.g., `com.yourname.musicmcp`)
5. Click **Continue** and **Register**

### 2. Create a MusicKit Private Key
#### 2. Create a MusicKit Private Key

1. Go to [Apple Developer > Keys](https://developer.apple.com/account/resources/authkeys/list)
2. Click **+** to create a new key
Expand All @@ -40,11 +87,11 @@ An MCP (Model Context Protocol) server that integrates with Apple Music, allowin
6. **Download the .p8 file** (you can only download it once!)
7. Note the **Key ID** shown on the page

### 3. Find Your Team ID
#### 3. Find Your Team ID

Your Team ID is visible at the top right of the Apple Developer portal, or under **Membership Details**.

## Installation
### Installation

```bash
git clone <this-repo>
Expand All @@ -53,7 +100,7 @@ npm install
npm run build
```

## Configuration
### Configuration

The server is configured via environment variables:

Expand All @@ -66,7 +113,7 @@ The server is configured via environment variables:
| `APPLE_MUSIC_CONFIG_DIR` | No | Config directory path (default: `~/.apple-music-mcp/`) |
| `APPLE_MUSIC_AUTH_PORT` | No | Port for auth server (default: `7829`) |

## Authorization
### Authorization

Before using library features (playlists, library songs, recommendations), you need to authorize with your Apple Music account:

Expand All @@ -85,7 +132,7 @@ This will:

**Note:** Catalog search works without authorization. Only library/personal features require it.

## Adding to Claude Desktop
### Adding to Claude Desktop (Local)

Add this to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):

Expand All @@ -106,7 +153,7 @@ Add this to your Claude Desktop config (`~/Library/Application Support/Claude/cl
}
```

## Adding to Claude Code
### Adding to Claude Code (Local)

Add to your Claude Code settings (`.claude/settings.json` or global settings):

Expand Down Expand Up @@ -213,8 +260,10 @@ npm start # Run compiled version

## Architecture

- **Transport:** stdio (JSON-RPC over stdin/stdout)
- **Local mode:** stdio transport (JSON-RPC over stdin/stdout)
- **Remote mode:** Stateless HTTP on Vercel via `WebStandardStreamableHTTPServerTransport`
- **API:** Direct REST calls to `api.music.apple.com/v1/`
- **Auth:** ES256 JWT developer tokens + browser-based MusicKit JS for user tokens
- **Auth:** ES256 JWT developer tokens (signed with `jose`) + browser-based MusicKit JS for user tokens
- **Multi-user:** Music User Token passed per-request as a header (no server-side storage)
- **Caching:** In-memory with per-endpoint TTLs
- **Error handling:** Typed errors with helpful messages and retry logic for rate limits
46 changes: 46 additions & 0 deletions api/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { loadVercelConfig } from "../src/config-vercel.js";
import { generateDeveloperTokenJose } from "../src/auth/developer-token-jose.js";

// Cache developer token across warm invocations
let cachedDevToken: string | null = null;
let tokenExpiry = 0;

async function getDeveloperToken(): Promise<string> {
const now = Date.now();
if (cachedDevToken && now < tokenExpiry) {
return cachedDevToken;
}
const config = loadVercelConfig();
cachedDevToken = await generateDeveloperTokenJose(
config.teamId,
config.keyId,
config.privateKeyPem
);
tokenExpiry = now + 23 * 60 * 60 * 1000;
return cachedDevToken;
}

export default {
async fetch(_request: Request): Promise<Response> {
try {
const devToken = await getDeveloperToken();

// Read the static HTML and inject the developer token as a meta tag
const htmlPath = join(process.cwd(), "public", "auth.html");
let html = readFileSync(htmlPath, "utf8");
html = html.replace(
"</head>",
` <meta name="developer-token" content="${devToken}">\n</head>`
);

return new Response(html, {
status: 200,
headers: { "Content-Type": "text/html; charset=utf-8" },
});
} catch (err: unknown) {
return new Response(`Auth page error: ${String(err)}`, { status: 500 });
}
},
};
106 changes: 106 additions & 0 deletions api/mcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { loadVercelConfig } from "../src/config-vercel.js";
import { generateDeveloperTokenJose } from "../src/auth/developer-token-jose.js";
import { AppleMusicClient } from "../src/api/client.js";
import { TokenStore } from "../src/auth/token-store.js";
import { registerAllTools } from "../src/tools/index.js";

// Cache developer token across warm invocations (same Vercel container)
let cachedDevToken: string | null = null;
let tokenExpiry = 0;

async function getDeveloperToken(
teamId: string,
keyId: string,
privateKeyPem: string
): Promise<string> {
const now = Date.now();
if (cachedDevToken && now < tokenExpiry) {
return cachedDevToken;
}
cachedDevToken = await generateDeveloperTokenJose(teamId, keyId, privateKeyPem);
tokenExpiry = now + 23 * 60 * 60 * 1000; // regenerate every 23 hours
return cachedDevToken;
}

function createEnvTokenStore(
devToken: string,
userToken?: string
): TokenStore {
return {
getDeveloperToken: () => devToken,
getMusicUserToken: () => userToken,
isDeveloperTokenExpired: () => false,
isUserTokenExpired: () => false,
setDeveloperToken: () => {},
setMusicUserToken: () => {},
} as unknown as TokenStore;
}

const CORS_HEADERS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
"Access-Control-Allow-Headers":
"Content-Type, Authorization, Music-User-Token, mcp-session-id, Last-Event-ID, mcp-protocol-version",
"Access-Control-Expose-Headers": "mcp-session-id, mcp-protocol-version",
};

async function handleMcpRequest(request: Request): Promise<Response> {
// Handle CORS preflight
if (request.method === "OPTIONS") {
return new Response(null, { status: 204, headers: CORS_HEADERS });
}

// Bearer token auth
const apiKey = process.env.MCP_API_KEY;
if (apiKey) {
const authHeader = request.headers.get("Authorization");
if (authHeader !== `Bearer ${apiKey}`) {
return new Response("Unauthorized", { status: 401, headers: CORS_HEADERS });
}
}

const config = loadVercelConfig();
const devToken = await getDeveloperToken(
config.teamId,
config.keyId,
config.privateKeyPem
);

// User token: prefer request header, fall back to env var
const musicUserToken =
request.headers.get("Music-User-Token") || config.musicUserToken;

const tokenStore = createEnvTokenStore(devToken, musicUserToken);
const client = new AppleMusicClient(tokenStore, devToken, config.storefront);

const server = new McpServer({
name: "apple-music-remote",
version: "1.0.0",
});

registerAllTools(server, client);

const transport = new WebStandardStreamableHTTPServerTransport();
await server.connect(transport);

const response = await transport.handleRequest(request);

// Add CORS headers to the response
const headers = new Headers(response.headers);
for (const [key, value] of Object.entries(CORS_HEADERS)) {
headers.set(key, value);
}

return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
}

// Vercel Web Standard fetch export
export default {
fetch: handleMcpRequest,
};
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
},
"scripts": {
"build": "tsc && cp -r src/auth-page dist/auth-page",
"vercel-build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts",
"auth": "tsx src/auth/user-token.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0",
"jose": "^6.1.3",
"jsonwebtoken": "^9.0.2",
"open": "^10.1.0",
"zod": "^3.24.0"
Expand Down
Loading