Skip to main content
Glama
SoundCloud MCP Server v2
by kosoymiki
README.md
# SoundCloud MCP Server v2 — uploads, studio, web-session automation

Production-grade SoundCloud MCP server: **55 MCP tools**, a browser control
panel, a local audio lab (ffmpeg), and a web-session uploader that works even
when SoundCloud API app registration is unavailable (Artist Pro gating).

- **MCP transport:** Streamable HTTP (`/mcp`) with bearer tokens + OAuth discovery.
- **Panel:** `http://localhost:3000/panel` (7 tabs, nonce CSP, CSRF-protected).
- **Official API path:** `api.soundcloud.com` with a registered app (OAuth).
- **Web-session path:** drives the real `soundcloud.com` uploader/actions via a
  logged-in Chromium session — no file picker, presigned S3 upload, private
  server actions discovered from the live bundle.
- **Audio lab:** probe, ID3 edit, trim, speed/pitch (rubberband), loudness,
  convert — plus analysis/boundary tooling for slowed/edited masters.

## Start here (5 minutes)

```bash
# private repo: add the friend as a collaborator, or send a source tarball
git clone <repo> soundcloud-mcp-server && cd soundcloud-mcp-server
bash install-termux.sh        # Termux: pkg deps + npm ci + build + 81 tests
# non-Termux: npm ci && npm run build && npm test
npm run verify                # typecheck + tests + build
```

Then edit `.env`, choose authorization (A or B below), `npm start`, and open
`http://localhost:3000/panel`.

**Sharing it with someone:** the repository is private, so either add them as a
collaborator (`gh api repos/<owner>/soundcloud-mcp-server/collaborators/<user> -X PUT -f permission=push`)
or hand them a source archive (`git archive --format=tar.gz --prefix=soundcloud-mcp-server/ HEAD -o soundcloud-mcp-server.tar.gz`)
— the archive contains only tracked source files, no cookies or state.

**Project master skill (for AI agents):**
[`.opencode/skills/soundcloud-mcp-server/SKILL.md`](.opencode/skills/soundcloud-mcp-server/SKILL.md)
— a self-contained operating guide with the golden path, architecture map,
web-session pipeline, audio-lab conventions, troubleshooting table, security
rules and definition of done. It needs no external H2/agent framework; any model
that can read files can follow it.

## Requirements

| Component | Version / notes |
|---|---|
| Node.js | **>= 24** (`node:sqlite` is unflagged from Node 24) |
| npm | ships with Node |
| ffmpeg / ffprobe | for the audio lab and analysis scripts (`FFMPEG_PATH`, `FFPROBE_PATH`) |
| Chromium / Chrome | only for web-session tools (`SC_WEB_CHROME`, default `chromium-browser`) |
| SoundCloud account | official API app **or** exported web cookies |

Termux/Android: `pkg install nodejs ffmpeg chromium`, use `$PREFIX/tmp` for
scratch, and keep `DATA_DIR` on internal storage (chmod 700).

## Install, build, test

```bash
npm ci                 # or npm install
npm run build          # tsc -> dist/
npm test               # 81 unit tests
npm run typecheck
npm run e2e            # 16-check end-to-end harness (mock SoundCloud)
npm start              # node dist/index.js
```

`npm audit --omit=dev` is clean (0 vulnerabilities at release time).

## Dependencies

Runtime: `@modelcontextprotocol/sdk`, `express`, `busboy`, `zod`,
`puppeteer-core` (web-session automation).
Dev: `typescript`, `tsx`, `@types/node`, `@types/express`, `@types/busboy`.

## Configuration

All settings are environment variables (`.env.example` is the reference).

| Variable | Default | Meaning |
|---|---|---|
| `PORT` / `HOST` | `3000` / `127.0.0.1` | listen address |
| `PUBLIC_URL` | `http://localhost:PORT` | public origin; `https://` enables HSTS + Secure cookies |
| `MCP_MODE` | `both` | `local`, `oauth`, `both` |
| `MCP_TRUST_PROXY` | `false` | trust `X-Forwarded-*` (only behind a trusted reverse proxy) |
| `SC_CLIENT_ID` / `SC_CLIENT_SECRET` | — | SoundCloud app credentials (per device) |
| `SC_REDIRECT_URI` | `${PUBLIC_URL}/soundcloud/callback` | must be registered in the app |
| `SC_ACCESS_TOKEN` / `SC_REFRESH_TOKEN` | — | optional headless bootstrap |
| `SC_AUTH_SCHEME` | `OAuth` | `OAuth` (current) or `Bearer` (legacy apps) |
| `SC_API_BASE` / `SC_AUTH_BASE` | production | testing/proxy override |
| `SC_MEDIA_DIRS` | `~` | colon-separated upload/audio path allowlist |
| `SC_MAX_UPLOAD_BYTES` | 4 GiB | hard upload cap |
| `FFMPEG_PATH` / `FFPROBE_PATH` | `ffmpeg` / `ffprobe` | audio lab binaries |
| `AUDIO_WORK_DIR` | `DATA_DIR/edits` | allowed output directory |
| `AUDIO_MAX_CONCURRENT` | `2` | concurrent ffmpeg jobs |
| `DATA_DIR` | `~/.local/share/soundcloud-mcp` | encrypted state, keys, temp uploads (0700) |
| `MCP_ENCRYPTION_KEY` | generated | 32-byte base64 AES-256-GCM key (0600) |
| `MCP_LOCAL_TOKEN` | generated | bearer for non-OAuth MCP clients (0600) |
| `MCP_ACCESS_TOKEN_TTL` / `MCP_REFRESH_TOKEN_TTL` | 7 d / 30 d | server tokens |
| `MCP_SESSION_IDLE_TTL` | 30 min | idle MCP sessions |
| `MCP_PANEL_AUTO_LOGIN` | `true` | reuse a local connection for loopback requests |
| `MCP_OPEN_BROWSER` | `true` | allow `open_auth_window` / `npm run auth` |
| `UI_BRAND_COLOR` | `#f50` | panel accent |
| `LOG_LEVEL` / `MCP_DEBUG` | `info` / `false` | diagnostics |
| `SC_WEB_CHROME` | `chromium-browser` | Chrome/Chromium binary for web-session tools |
| `SC_WEB_COOKIE_FILE` | `DATA_DIR/chrome-soundcloud-cookies.json` | exported SoundCloud cookies (0600!) |
| `SC_WEB_UPLOAD_URL` | `https://soundcloud.com/n/upload` | page used for action discovery |
| `SC_WEB_DEBUG_PORT` | `9333` | CDP port (reuses an existing browser) |
| `SC_WEB_HEADLESS` | `true` | run the web-session browser headless |
| `SC_WEB_TIMEOUT_MS` | `90000` | navigation timeout |
| `SC_WEB_CLIENT_ID` | JWT claim | api-v2 client id used for verification |
| `CHROME_COOKIES_DB` | auto-detected | Chrome/Chromium cookie database used by `web-upload cookies` |
| `SC_WEB_TOKEN_URL` | `https://secure.soundcloud.com/oauth/token` | token endpoint for `cookies --refresh` |
| `SC_ALLOW_COOKIES_IN_REPO` | `0` | allow cookie exports inside a git work tree (unsafe default) |

## Authorization

### A. Official API (needs a registered app)

Create an app at `https://soundcloud.com/you/apps` (registration is currently
Artist Pro-only; otherwise see B) and register
`${PUBLIC_URL}/soundcloud/callback`. Then:

```bash
node scripts/sc.mjs auth --client-id "$SC_CLIENT_ID" --client-secret "$SC_CLIENT_SECRET"
```

The panel setup page (`/panel/setup`, loopback only) stores credentials
encrypted; the browser OAuth flow issues a server token in an HttpOnly cookie.
No SoundCloud token is ever pasted or copied.

### B. Web session (no API app required)

```bash
node dist/web/cli.js cookies        # fresh cookies from the local Chrome profile (0600)
node dist/web/cli.js doctor --probe # cookie/token/Chromium check + live account
node dist/web/cli.js selftest       # full chain, creates a private test track and deletes it
```

`cookies` reads Chrome/Chromium directly (on Android it stages the profile
through `su`), decrypts the OSCrypt `v10` values and writes
`DATA_DIR/chrome-soundcloud-cookies.json` with mode 0600. The file is the only
input the web session needs; `SC_WEB_COOKIE_FILE` overrides its path. Exports
never land inside a git work tree (override only with
`SC_ALLOW_COOKIES_IN_REPO=1`) and the repo `.gitignore` covers local state.

Runtime chain, all through SoundCloud's own web app:

```text
cookies -> live session (/me) -> empty album (createAlbumAction)
        -> createUploadPolicyAction -> presigned S3 PUT (MD5 checked)
        -> createTrackAction (private) -> addToPlaylistAction
        -> read-back (getTrackInfoAction + api-v2) -> delete track + album
```

`selftest` runs that chain on the real account and cleans up after itself
(`--keep` leaves the track/album for inspection). `upload` performs the same
upload leg for a real file, optionally into an existing album:

```bash
node dist/web/cli.js upload --file song.mp3 --title song --genre Electronic \
  --album-urn soundcloud:playlists:123456789 --verify
```

Cookies expire after roughly an hour: `cookies` re-exports them from the browser,
`cookies --refresh` exchanges the stored `oauth_refresh_token` instead (this
rotates the token SoundCloud's web client also holds, so prefer re-export). The
session refreshes an about-to-expire token by re-reading the browser profile
before falling back to the refresh token.

**Platform support for `cookies`:** Android (root, Chrome/Chromium) and
Linux (Chrome/Chromium) decrypt the `v10` OSCrypt values. On macOS/Windows the
browser uses Keychain/DPAPI, so automatic decryption is not implemented — export
cookies manually with any DevTools-based exporter (the JSON shape above) and
point `SC_WEB_COOKIE_FILE` at it.

## Connect an MCP client

The server speaks Streamable HTTP with a bearer token, so any MCP client that
supports HTTP transports can use it (Claude Desktop, Cursor, opencode, custom
SDKs):

```json
{
  "mcpServers": {
    "soundcloud": {
      "type": "http",
      "url": "http://localhost:3000/mcp",
      "headers": {
        "Authorization": "Bearer <contents of DATA_DIR/local-token>"
      }
    }
  }
}
```

- `DATA_DIR/local-token` is generated on first start (0600). Copy its value, not
  the file, into the client config.
- Modern clients (protocol `2026-07-28`) can point at `http://localhost:3000/mcp-v2`
  instead; the tool/resource/prompt surface is identical.
- Keep `HOST=127.0.0.1` unless you terminate TLS in front of the server; then set
  `PUBLIC_URL=https://…` and `MCP_TRUST_PROXY=true`.

## MCP tools (55)

**Profile (6):** `get_me`, `get_upload_quota`, `update_profile`,
`upload_avatar`, `get_panel_url`, `open_auth_window`

**Tracks & uploads (10):** `search_tracks`, `get_track`, `list_my_tracks`,
`update_track`, `delete_track`, `upload_track`, `upload_track_artwork`,
`get_stream_url`, `get_track_comments`, `post_comment`

**Playlists (8):** `list_my_playlists`, `get_playlist`, `create_playlist`,
`update_playlist`, `add_tracks_to_playlist`, `remove_track_from_playlist`,
`reorder_playlist`, `delete_playlist`

**Social & discovery (16):** `like_track`, `unlike_track`, `like_playlist`,
`unlike_playlist`, `repost_track`, `unrepost_track`, `follow_user`,
`unfollow_user`, `list_followers`, `list_followings`, `list_likes`,
`list_reposts`, `list_activity`, `search_users`, `search_playlists`,
`resolve_url`

**Audio lab (7):** `audio_probe`, `read_audio_tags`, `edit_audio_tags`,
`change_audio_speed`, `normalize_audio`, `trim_audio`, `convert_audio`

**Web session (8):** `web_upload_track` (optional `album_title` or
`album_urn`), `web_create_album`, `web_actions` (live action discovery),
`web_cookies_export` (fresh cookies from Chrome, values never returned),
`web_cookies_refresh` (exchange the stored refresh token),
`web_session_status` (cookie/token/Chromium health, `probe` opens the live page),
`web_delete_track`, `web_chain_selftest` (full chain with cleanup)

Lists accept `limit`, `max_items`, opaque `cursor`; IDs accept a numeric id,
`soundcloud:<kind>:<id>` URN, or an `api.soundcloud.com` URL.

## CLI

```bash
npm run cli -- auth|upload|tracks|panel        # scripts/sc.mjs
npm run web-upload -- upload --file song.mp3 --title "Song" --album "My Album"
npm run web-upload -- upload --file song.mp3 --title "Song" --album-urn soundcloud:playlists:<id>
npm run web-upload -- album --title "My Album" --tracks soundcloud:tracks:<id>
npm run web-upload -- actions                  # list live server actions
node dist/web/cli.js cookies                   # export fresh cookies from Chrome
node dist/web/cli.js doctor --probe            # live session + action count
node dist/web/cli.js selftest                  # full chain, cleans up
```

## Analysis & editing scripts

Generic, argument-driven tools (no session data baked in):

| Script | Purpose |
|---|---|
| `scripts/analyze-track.py <file>` | LUFS/peak/clipping, band energies, centroid, stereo correlation |
| `scripts/bpm.py <file>` | tempo estimate (spectral-flux autocorrelation) |
| `scripts/envelope.py <file> <start> <end>` | 20 ms RMS envelope |
| `scripts/fine-env.py <file> <start> <end> [ms]` | 5 ms RMS/peaks for cut points |
| `scripts/boundary.py <file> <start> <end>` | per-frame RMS/centroid/F0/ZCR |
| `scripts/speaker-change.py <file> <start> <end>` | MFCC window dissimilarity (speaker change) |
| `scripts/trim-track.mjs <in> <out> [start] [end]` | trim through the repo AudioService (ID3 preserved) |
| `scripts/replace-track.mjs <oldId> <file> <title> <dur> [albumUrn]` | delete + re-upload + optionally add to album |
| `scripts/add-to-album-action.mjs <playlistUrn> <trackUrn>` | add a track to an existing album |
| `scripts/page-actions.mjs <url>` | list server actions discovered on a page |
| `scripts/web-smoke.mjs [trackUrn] [permalink]` | live web-session smoke test |
| `scripts/s3-put.mjs <policy.json> <file>` | PUT a file to a presigned upload policy |
| `scripts/stealth-import-cookies.py` | import/export browser cookies for web-session tools |
| `scripts/sc.mjs` / `scripts/e2e.mjs` | CLI and mock E2E harness |

Analysis examples:

```bash
python3 scripts/analyze-track.py track.mp3
python3 scripts/bpm.py track.mp3
python3 scripts/fine-env.py track.mp3 116.8 118.2 5
python3 scripts/speaker-change.py track.mp3 114 120
```

`trim-track.mjs` and the audio-lab tools resolve paths through `SC_MEDIA_DIRS`;
include the input directory, e.g.
`SC_MEDIA_DIRS=~/Music:~ npm run ...`.

## Browser panel

`${PUBLIC_URL}/panel` (login `/panel/login`, setup `/panel/setup` — loopback
only) provides: Overview, Upload (drag & drop queue), Tracks, Playlists,
Social, Audio lab, Settings. Mutations require the CSRF header; pages use a
nonce CSP and no-store caching.

## MCP protocol support

Two endpoints, same 55 tools, resource and prompt, same auth:

- **`/mcp`** — legacy Streamable HTTP (**2025-11-25**) via `@modelcontextprotocol/sdk` 1.32.2.
- **`/mcp-v2`** — modern era (**2026-07-28**) via `@prefecthq/fastmcp-ts` 1.9.1, with tools
  registered per SoundCloud connection (handlers keep their bound `ToolContext`).
  Modern clients send `MCP-Protocol-Version: 2026-07-28`, the `Mcp-Method`
  header, the `Mcp-Name` header for `resources/read` / `prompts/get`, and the
  `io.modelcontextprotocol/{protocolVersion,clientInfo,clientCapabilities}` `_meta`
  envelope.

Both endpoints expose the `soundcloud://guide` resource and the `plan_upload`
prompt, from one definition in `src/mcp/resources.ts`, and share the same
bearer/OAuth authentication, rate limits, CORS allowlist and security headers.

## Security

- Tokens/secrets: AES-256-GCM state, 0600 files, 0700 dirs; logger redacts
  `access_token`/`refresh_token`/`client_secret`.
- Auth: OAuth PKCE + one-time state; server-issued tokens (hashed at rest);
  constant-time local-token compare.
- Panel: HttpOnly/SameSite=Lax cookies (`Secure` on TLS), Origin + `x-scmcp-csrf`
  on cookie-authenticated mutations, nonce CSP, frame-ancestors none.
- HTTP: HSTS when `PUBLIC_URL=https`, `X-Content-Type-Options`, `X-Frame-Options:
  DENY`, `Referrer-Policy: no-referrer`, `Permissions-Policy`; CORS allowlist
  (`PUBLIC_URL` + localhost) instead of `*`; rate limits on `/mcp`, OAuth and
  `/panel/auth`.
- Inputs: media path allowlist via `realpath`, body limits, ffmpeg/ffprobe
  spawned with argument arrays (no shell), no `eval` on user input.
- Web session: CDP bound to `127.0.0.1`; cookie file permission warning when
  group/other-readable.
- MCP 2026-07-28 security/operational matrix: [`docs/SECURITY-2026-11.md`](docs/SECURITY-2026-11.md).
- Audit: the 2026-11-10 byte-level review (P0–P4, every module) and its fixes are
  in [`docs/AUDIT-2026-11-10.md`](docs/AUDIT-2026-11-10.md). Re-run
  `npm run verify && npm run e2e` after any change.

## Project layout

| Path | Role |
|---|---|
| `src/server.ts` | Express app, security headers/CORS, rate limits, MCP endpoint |
| `src/mcp/` | MCP server factory + session manager |
| `src/tools/` | MCP tool registrations (profile/tracks/playlists/social/audio/web) |
| `src/soundcloud/` | API client, HTTP, multipart, ID3 |
| `src/auth/` | OAuth/PKCE, encrypted store, token service, crypto |
| `src/uploads/` | staging + upload service |
| `src/audio/` | ffmpeg engine, ID3 file writer, audio service |
| `src/panel/` | panel router/REST API + single-file UI |
| `src/web/` | web-session transport (CDP/actions), uploader, CLI |
| `tests/` | 81 unit tests (`npm test`) |
| `scripts/` | CLI, E2E, cookie import, analysis/editing tools |
| `docs/` | architecture, API notes, security/web-upload guides |

## Troubleshooting

- **`Application registration is not available`** — app creation is Artist
  Pro-gated; use the web-session path (B).
- **`409 x-nextjs-action-not-found`** — stale action id; rediscover with
  `web_actions` / `page-actions.mjs`.
- **Web drop/click does nothing** — the OneTrust consent dialog can cover the
  uploader; the session dismisses it (`#accept-recommended-btn-handler`/Escape).
- **DataDome** — direct GraphQL/playlist writes from the browser can be
  challenged; the server-action path is the supported route. No bypass is done.
- **Permalink change** — old profile/track URLs 404 (SoundCloud does not
  redirect after a permalink change).
- **`api-v2` verification 401** — set `SC_WEB_CLIENT_ID`.

## License

AGPL-3.0-or-later. Original work Copyright (C) 2026 Marcell Kehmstedt;
fork contributions under the same license.