This project is built with Claude Code — contributions via vibe coding are welcome.
- Python 3.14+
- uv —
curl -LsSf https://astral.sh/uv/install.sh | sh
git clone https://github.com/nelsonra/mixcloud-mcp.git
cd mixcloud-mcp
uv sync
cp .env.example .envEdit .env and add your credentials. There are two authentication paths — use whichever fits your workflow:
Option A — OAuth (recommended, required for uploads):
Register an app at mixcloud.com/developers. Set the redirect URI to http://localhost:4000/oauth/callback, then add to .env:
MIXCLOUD_CLIENT_ID=your_client_id
MIXCLOUD_CLIENT_SECRET=your_client_secret
When you start the HTTP server or run stdio mode, the OAuth flow is handled automatically — see the sections below.
Option B — Static token:
Run the one-time OAuth CLI, approve in the browser, and the token is written to .env:
uv run mixcloud-mcp-oauthstdio (as Claude Desktop would run it):
uv run mixcloud-mcpIf MIXCLOUD_CLIENT_ID and MIXCLOUD_CLIENT_SECRET are set, a sidecar starts on port 4000 and the browser opens automatically to the Mixcloud OAuth consent page. After you approve, the token is saved to ~/.config/mixcloud-mcp/.env and persists across restarts.
If the browser does not open (e.g. headless), visit http://localhost:4000/oauth/authorize manually.
Running in Claude Desktop from a local checkout:
Add this to your claude_desktop_config.json (Mac: ~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"mixcloud": {
"command": "/Users/yourname/.local/bin/uv",
"args": [
"run",
"--directory",
"/path/to/mixcloud-mcp",
"mixcloud-mcp"
],
"env": {
"MIXCLOUD_CLIENT_ID":"YOUR_MIXCLOUD_CLIENT_ID",
"MIXCLOUD_CLIENT_SECRET":"YOUR_MIXCLOUD_CLIENT_SECRET",
"MCP_API_KEY":"YOUR_MCP_API_KEY",
"MCP_PORT":"YOUR_MCP_PORT",
"UPLOAD_PORT":"YOUR_UPLOAD_PORT"
}
}
}
}--directorymust point to the project root (wherepyproject.tomllives), not tosrc/mixcloud_mcp/. Pointing at thesrcsubdirectory puts it onsys.pathdirectly, which shadows Python's stdlibhttpmodule and crashes on import.- Run
which uvto get the full path to uv — Claude Desktop does not inherit your shell PATH.
HTTP server:
MCP_PUBLIC_URL=http://localhost:8000 uv run mixcloud-mcp-httpMCP_PUBLIC_URL must be set when using OAuth — it tells Mixcloud where to redirect after login (<MCP_PUBLIC_URL>/oauth/callback). For local dev, http://localhost:8000 works as long as you registered that callback URI in your Mixcloud app.
Without OAuth, generate a static API key instead:
uv run mixcloud-mcp-keygen
# → add MCP_API_KEY=<output> to .env
uv run mixcloud-mcp-httpTo skip auth entirely for quick local testing:
DISABLE_AUTH=true uv run mixcloud-mcp-httpMCP Inspector gives you a browser UI to call tools and inspect responses interactively.
stdio tools (no server needed):
uv run fastmcp dev inspector src/mixcloud_mcp/server.py:mcpHTTP transport — start the server first, then open the inspector:
MCP_PUBLIC_URL=http://localhost:8000 uv run mixcloud-mcp-httpuv run fastmcp dev inspector src/mixcloud_mcp/server.py:mcpIn the Inspector browser UI, switch transport to Streamable HTTP and enter http://localhost:8000/mcp.
The upload tool uses an embedded React UI rendered as an MCP App. To preview it locally:
1. Start the HTTP server:
MCP_PUBLIC_URL=http://localhost:8000 uv run mixcloud-mcp-http2. Start the MCP Apps preview in a second terminal:
uv run fastmcp dev apps src/mixcloud_mcp/server.pyThis opens a browser where you can call the upload_cloudcast tool — the upload form renders as an interactive UI. The form posts directly to http://localhost:8000/upload on the running server.
Make sure
MIXCLOUD_ACCESS_TOKENis set in.envbefore testing uploads.
If you rebuild the React app (npm run build in mcp-app/), commit the updated src/mixcloud_mcp/static/mcp-app.html and restart the HTTP server to pick up the new bundle.
The HTTP transport is intended for remote MCP clients. To test it end-to-end with Claude:
1. Start the HTTP server with OAuth:
MCP_PUBLIC_URL=http://localhost:8000 uv run mixcloud-mcp-httpOr with DISABLE_AUTH=true if you want to skip auth during initial testing:
DISABLE_AUTH=true uv run mixcloud-mcp-http2. Expose it publicly with ngrok:
ngrok http 8000ngrok prints a URL like https://xxxx.ngrok-free.app. Copy it.
3. Add as a custom connector in Claude
In Claude, go to Settings → Connectors → Add custom connector and enter:
https://xxxx.ngrok-free.app/mcp
With OAuth configured, Claude will prompt you to authenticate via Mixcloud on first connect. With DISABLE_AUTH=true, the tools are available immediately.
When using ngrok for OAuth testing, set
MCP_PUBLIC_URL=https://xxxx.ngrok-free.appand register that callback URI in your Mixcloud app.
| Mode | Redirect URI to register in Mixcloud app | Env var to set |
|---|---|---|
| stdio (local) | http://localhost:4000/oauth/callback |
UPLOAD_PORT=4000 (default) |
| HTTP (local dev) | http://localhost:8000/oauth/callback |
MCP_PUBLIC_URL=http://localhost:8000 |
| HTTP (hosted) | https://your-domain.com/oauth/callback |
MCP_PUBLIC_URL=https://your-domain.com |
| HTTP (ngrok) | https://xxxx.ngrok-free.app/oauth/callback |
MCP_PUBLIC_URL=https://xxxx.ngrok-free.app |
mixcloud-mcp/
├── mcp-app/ ← React upload UI (MCP App)
│ ├── src/
│ │ ├── mcp-app.tsx ← upload form component
│ │ └── useMixcloudUploader.ts
│ └── dist/ ← intermediate build output (gitignored)
├── docs/
│ └── adr/ ← Architecture Decision Records
└── src/mixcloud_mcp/
├── server.py ← FastMCP app — registers all tools and resources
├── stdio.py ← stdio entry point (starts sidecar if OAuth configured)
├── http_server.py ← HTTP entry point (Starlette wrapper, OAuth proxy, rate limiting)
├── auth.py ← MixcloudOAuthProxy + MixcloudTokenVerifier
├── sidecar.py ← lightweight sidecar for stdio mode (OAuth + upload)
├── keygen.py ← mixcloud-mcp-keygen CLI
├── oauth.py ← mixcloud-mcp-oauth CLI (one-time token flow, no credentials needed)
├── upload_log.py ← in-memory upload result log (deque, last 20 entries)
├── api/
│ └── client.py ← Mixcloud API wrapper (injects token from session or env)
├── routes/
│ └── upload.py ← POST /upload — receives multipart form, forwards to Mixcloud
└── tools/
├── search/ ← search_cloudcasts
├── tracks/ ← get_cloudcast
├── users/ ← get_user, get_user_cloudcasts, get_user_followers, get_user_following
└── upload/ ← upload_cloudcast (MCP App UI tool)
Each tool category follows the same pattern:
__init__.py— barrel that calls each tool'sregister(mcp)types.py— Pydantic models for the API response- one file per tool (e.g.
get_user.py) — implements and registers the tool
- Create a file in the relevant
tools/<category>/folder - Define a
register(mcp: FastMCP) -> Nonefunction with a@mcp.tool()decorated async function inside it - Add the register call to that category's
__init__.py
Example:
# src/mixcloud_mcp/tools/users/get_user_favorites.py
import json
from fastmcp import FastMCP
from mixcloud_mcp.api.client import mixcloud_get
def register(mcp: FastMCP) -> None:
@mcp.tool()
async def get_user_favorites(username: str, limit: int = 20) -> str:
"""Get a user's favourite mixes on Mixcloud."""
clean = username.strip().strip("/").strip()
raw = await mixcloud_get(f"/{clean}/favorites/", params={"limit": limit})
return json.dumps(raw.get("data", []), indent=2)Then add to tools/users/__init__.py:
from .get_user_favorites import register as register_get_user_favorites
def register(mcp: FastMCP) -> None:
...
register_get_user_favorites(mcp)- Create a new folder under
tools/(e.g.tools/playlists/) - Add
__init__.py,types.py, and your tool files - Import and call
registerinserver.py
If you're coming from a TypeScript/Node background:
| Node / TypeScript | Python |
|---|---|
npm install <pkg> |
uv add <pkg> |
npx / npm run |
uv run |
async/await |
async/await (same!) |
fetch() |
httpx.AsyncClient() |
| Zod schemas | Pydantic BaseModel |
z.object({...}) |
class Foo(BaseModel): ... |
interface Foo {} |
class Foo(BaseModel): ... |